Files
whatsapp/modules/turnero/views/recepcion.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 2dec300f3a Fix professional signature row only showing when form schema requires it
Only display the 'Falta firma enfermero' row and button when the assigned
consent form actually contains a firma_profesional field in its esquema.
get_consentimientos.php now computes requiere_firma_profesional from the
JSON schema and recepcion.php gates the row on that flag.

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

2208 lines
107 KiB
PHP

<?php
/**
* Puesto de Recepción — Módulo Turnero
* Requiere login + módulo turnero.
*/
require_once __DIR__ . '/../../../config/config.php';
if (!isUserLoggedIn()) {
header('Location: ' . BASE_URL . 'login.php');
exit;
}
// Cargar catálogos para la ficha
try {
$pdo = Database::getInstance()->getConnection();
$lugarRecepcion = $pdo->query(
"SELECT id, nombre FROM turnero_lugares WHERE tipo='recepcion' AND activo=1 ORDER BY sort_order LIMIT 1"
)->fetch(PDO::FETCH_ASSOC);
// Destinos agrupados: un representante por grupo_id, individuales si grupo_id=NULL
$lugaresDestino = $pdo->query(
"SELECT MIN(id) AS id,
CASE WHEN grupo_id IS NOT NULL THEN 'Toma de Muestras' ELSE nombre END AS nombre,
grupo_id
FROM turnero_lugares
WHERE tipo='muestras' AND activo=1
GROUP BY COALESCE(grupo_id, id)
ORDER BY MIN(sort_order)"
)->fetchAll(PDO::FETCH_ASSOC);
$lugarMuestras = $lugaresDestino[0] ?? null;
$examenes = $pdo->query(
"SELECT id, codigo, nombre, categoria FROM exam_tipos WHERE activo = 1 ORDER BY categoria, nombre"
)->fetchAll(PDO::FETCH_ASSOC);
// Agrupar exámenes por categoría
$examenesAgrupados = [];
foreach ($examenes as $ex) {
$cat = $ex['categoria'] ?: 'General';
$examenesAgrupados[$cat][] = $ex;
}
// Escritorio de recepción (vía ?desk_id=X)
$deskId = (int)($_GET['desk_id'] ?? 0);
$desk = null;
$desksRecepcion = $pdo->query(
"SELECT id, nombre FROM turnero_lugares WHERE tipo='recepcion' AND activo=1 ORDER BY sort_order"
)->fetchAll(PDO::FETCH_ASSOC);
if ($deskId) {
foreach ($desksRecepcion as $d) {
if ($d['id'] === $deskId) { $desk = $d; break; }
}
if (!$desk) { $deskId = 0; }
}
// Si solo hay un escritorio, asignarlo automáticamente
if (!$deskId && count($desksRecepcion) === 1) {
$desk = $desksRecepcion[0];
$deskId = (int)$desk['id'];
}
} catch (\Throwable $e) {
$lugares = [];
$examenesAgrupados = [];
$deskId = 0;
$desk = null;
$lugarRecepcion = null;
$lugarMuestras = null;
}
$deskNombre = $desk['nombre'] ?? null; // null = vista genérica
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($deskNombre ?? 'Recepción') ?> — Turnero</title>
<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 de dos columnas ── */
.rec-layout {
display: grid;
grid-template-columns: 320px 1fr;
height: calc(100vh - 56px);
overflow: hidden;
}
@media (max-width: 900px) {
.rec-layout { grid-template-columns: 1fr; position: relative; overflow: hidden; }
.rec-cola { transition: transform .2s ease; }
.rec-cola.mobile-oculta { transform: translateX(-100%); position: absolute; inset: 0; pointer-events: none; }
.rec-ficha { position: absolute; inset: 0; background: #fff; z-index: 10; transform: translateX(100%); transition: transform .2s ease; overflow-y: auto; }
.rec-ficha.mobile-visible { transform: translateX(0); }
.btn-volver-cola { display: inline-flex !important; align-items: center; gap: 6px; }
}
/* ── Columna cola ── */
.rec-cola {
background: #f8fafc;
border-right: 1px solid #e2e8f0;
display: flex; flex-direction: column;
overflow: hidden;
}
.rec-cola-header {
padding: 1rem 1.2rem .6rem;
background: #fff; border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
}
.cola-items { flex: 1; overflow-y: auto; padding: .5rem; }
.cola-card {
background: #fff; border: 1px solid #e2e8f0;
border-radius: 10px; padding: .65rem .9rem;
margin-bottom: .4rem; cursor: default;
display: flex; align-items: center; gap: .6rem;
transition: box-shadow .15s;
}
.cola-card.activo { border-color: #3b82f6; box-shadow: 0 0 0 2px #bfdbfe; }
.cola-card .prio-dot {
width: 36px; height: 36px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; font-weight: 800; color: #fff; flex-shrink: 0;
}
.cola-card .turno-info { flex: 1; min-width: 0; }
.cola-card .turno-info .cod { font-size: 1rem; font-weight: 700; }
.cola-card .turno-info .pac { font-size: .78rem; color: #64748b;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cola-vacia { text-align: center; padding: 3rem 1rem; color: #94a3b8; }
/* ── Columna ficha ── */
.rec-ficha {
overflow-y: auto; padding: 1.5rem;
background: #fff;
}
.ficha-placeholder {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
height: 100%; color: #94a3b8; text-align: center;
}
/* ── Secciones de la ficha ── */
.ficha-section {
background: #f8fafc; border: 1px solid #e2e8f0;
border-radius: 12px; padding: 1.2rem; margin-bottom: 1rem;
}
.ficha-section h6 { color: #475569; font-size: .78rem;
text-transform: uppercase; letter-spacing: .08em; margin-bottom: .8rem; }
/* ── Búsqueda de paciente ── */
.pac-resultado {
border: 1px solid #e2e8f0; border-radius: 8px; padding: .5rem .75rem;
cursor: pointer; margin-top: .25rem; transition: background .12s;
}
.pac-resultado:hover { background: #f1f5f9; }
.pac-resultado .pac-nombre { font-weight: 600; font-size: .9rem; }
.pac-resultado .pac-doc { font-size: .78rem; color: #64748b; }
.pac-seleccionado {
background: #eff6ff; border: 1px solid #93c5fd;
border-radius: 8px; padding: .65rem .9rem;
display: flex; align-items: center; justify-content: space-between;
}
/* ── Checkboxes de exámenes ── */
.exam-group-title { font-size: .75rem; color: #94a3b8;
text-transform: uppercase; letter-spacing: .08em; margin: .6rem 0 .3rem; }
.exam-check-item { display: flex; align-items: center; gap: .5rem;
padding: .3rem .4rem; border-radius: 6px; cursor: pointer;
transition: background .1s; font-size: .88rem; }
.exam-check-item:hover { background: #f1f5f9; }
.exam-check-item input[type=checkbox] { cursor: pointer; }
/* ── Consentimientos ── */
.consent-item {
display: flex; align-items: center; gap: .5rem;
padding: .5rem .7rem; border-radius: 10px;
font-size: .84rem; margin-bottom: .4rem;
border: 1px solid transparent;
}
.consent-item.firmado { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
.consent-item.pendiente { background: #fffbeb; color: #92400e; border-color: #fde68a; }
.consent-item.enviado { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
.consent-item.visto { background: #faf5ff; color: #6b21a8; border-color: #ddd6fe; }
.consent-item.rechazado { background: #f8fafc; color: #64748b; border-color: #e2e8f0; }
.consent-item .c-nom { flex: 1; font-weight: 500; }
.consent-item .c-badge { font-size: .7rem; font-weight: 700; padding: 1px 8px;
border-radius: 99px; border: 1px solid currentColor; opacity: .85; }
.consent-acciones { display: flex; gap: .3rem; flex-shrink: 0; }
.consent-acciones .btn { font-size: .72rem; padding: 2px 8px; border-radius: 7px; }
.consent-prof-row { display:flex; align-items:center; gap:.4rem; font-size:.75rem;
margin-top:.25rem; padding-top:.25rem; border-top:1px solid rgba(0,0,0,.07); }
.consent-prof-row .badge { font-size:.65rem; }
/* ── Barra de acciones ── */
.ficha-acciones {
position: sticky; bottom: 0;
background: #fff; border-top: 1px solid #e2e8f0;
padding: 1rem 0 .25rem; margin-top: 1rem;
display: flex; gap: .5rem; flex-wrap: wrap;
}
/* ── Badge de turno activo en topbar ── */
.badge-turno-activo {
background: #eff6ff; color: #1d4ed8;
border: 1px solid #bfdbfe; border-radius: 8px;
padding: .25rem .75rem; font-size: .82rem; font-weight: 700;
}
/* ── Toast ── */
.rec-toast {
position: fixed; bottom: 2rem; right: 2rem; z-index: 9999;
display: flex; align-items: center; gap: .7rem;
padding: .8rem 1.2rem; border-radius: 12px;
background: #fff; border: 1px solid #e2e8f0;
box-shadow: 0 8px 30px rgba(0,0,0,.12);
font-size: .9rem; font-weight: 500;
transform: translateY(120%); opacity: 0;
transition: transform .3s ease, opacity .3s ease;
pointer-events: none;
}
.rec-toast.show {
transform: translateY(0); opacity: 1;
}
.rec-toast.success { border-left: 4px solid #22c55e; }
.rec-toast.info { border-left: 4px solid #3b82f6; }
.rec-toast.warn { border-left: 4px solid #f59e0b; }
.rec-toast.error { border-left: 4px solid #ef4444; }
.rec-toast .ico { font-size: 1.2rem; }
.rec-toast.success .ico { color: #22c55e; }
.rec-toast.info .ico { color: #3b82f6; }
.rec-toast.warn .ico { color: #f59e0b; }
.rec-toast.error .ico { color: #ef4444; }
/* ── Historial del paciente ── */
.hist-toggle-btn {
display: flex; align-items: center; gap: 6px;
width: 100%; padding: 6px 10px; margin-top: 8px;
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
font-size: .78rem; font-weight: 600; color: #475569;
cursor: pointer; text-align: left; transition: background .15s;
}
.hist-toggle-btn:hover { background: #f1f5f9; }
.hist-toggle-btn .hist-chev { margin-left: auto; transition: transform .2s; }
.hist-toggle-btn.open .hist-chev { transform: rotate(180deg); }
.hist-pac-body {
border: 1px solid #e2e8f0; border-top: none;
border-radius: 0 0 8px 8px; background: #fff;
max-height: 260px; overflow-y: auto;
}
.pac-timeline { padding: 10px 12px; }
.timeline-item {
display: flex; gap: 10px; align-items: flex-start;
padding-bottom: 10px; position: relative;
}
.timeline-item:not(:last-child)::before {
content: ''; position: absolute; left: 6px; top: 14px;
width: 2px; bottom: 0; background: #e2e8f0;
}
.timeline-dot {
width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
margin-top: 2px; border: 2px solid #fff;
box-shadow: 0 0 0 2px currentColor;
}
.timeline-content { flex: 1; min-width: 0; }
.timeline-fecha { font-size: .68rem; color: #94a3b8; }
.timeline-codigo { font-size: .82rem; font-weight: 700; }
.timeline-lugar { font-size: .73rem; color: #64748b; }
.timeline-eb { font-size: .62rem; padding: 1px 7px; border-radius: 99px; font-weight: 700; display: inline-block; margin-top: 2px; }
</style>
</head>
<body>
<?php
$SIDEBAR_TITLE = 'Turnero';
$SIDEBAR_ICON = 'fas fa-ticket-alt';
require_once __DIR__ . '/../../../shared/components/sidebar.php';
?>
<?php if (!$deskId && count($desksRecepcion) > 1): ?>
<!-- Modal selector de escritorio (se muestra si no hay desk_id en URL) -->
<div class="modal fade" id="modalDeskSelect" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background:var(--doc-color,#1565c0);color:#fff">
<h5 class="modal-title"><i class="fas fa-desktop me-2"></i>Seleccione su escritorio</h5>
</div>
<div class="modal-body pb-2">
<p class="text-muted small mb-3">¿Desde qué escritorio de recepción está trabajando hoy?</p>
<div class="d-grid gap-2">
<?php foreach ($desksRecepcion as $d): ?>
<a href="?m=turnero&v=recepcion&desk_id=<?= $d['id'] ?>"
class="btn btn-outline-primary btn-lg">
<i class="fas fa-chair me-2"></i><?= htmlspecialchars($d['nombre']) ?>
</a>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
new bootstrap.Modal(document.getElementById('modalDeskSelect')).show();
});
</script>
<?php endif; ?>
<main class="main-content" style="padding:0">
<!-- ── Top bar ──────────────────────────────────────────── -->
<div class="content-header d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
style="position:sticky;top:0;z-index:100">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-concierge-bell text-primary"></i>
<strong><?= htmlspecialchars($deskNombre ?? 'Recepción') ?></strong>
<span id="badge-turno-activo" class="badge-turno-activo d-none">
Turno: <span id="badge-codigo">—</span>
</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
<?php if ($deskId): ?>
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=display_recepcion&desk_id=<?= $deskId ?>" class="btn btn-outline-info btn-sm" target="_blank">
<i class="fas fa-tv me-1"></i>Ver Pantalla
</a>
<?php endif; ?>
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
<i class="fas fa-bell me-1"></i>Llamar siguiente
</button>
</div>
</div>
<div class="rec-layout">
<!-- ══ Columna izquierda: cola ════════════════════════ -->
<div class="rec-cola">
<div class="rec-cola-header">
<div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold text-secondary small">
<i class="fas fa-list-ol me-1"></i>EN ESPERA
</span>
<span class="badge bg-primary rounded-pill" id="badge-count">0</span>
</div>
</div>
<div class="cola-items" id="cola-items">
<div class="cola-vacia">
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
Cola vacía
</div>
</div>
</div>
<!-- ══ Columna derecha: ficha del turno ═══════════════ -->
<div class="rec-ficha" id="rec-ficha">
<button class="btn btn-sm btn-outline-secondary btn-volver-cola mb-3"
style="display:none" onclick="volverACola()">
<i class="fas fa-arrow-left"></i> Cola
</button>
<div class="ficha-placeholder" id="ficha-placeholder">
<i class="fas fa-ticket-alt fa-3x mb-3" style="color:#cbd5e1"></i>
<p class="mb-0 fw-semibold">Sin turno activo</p>
<small class="text-muted">Haga clic en <i class="fas fa-bell"></i> de un turno para llamarlo, o use "Llamar siguiente"</small>
</div>
<!-- Ficha de turno (oculta hasta llamar) -->
<div id="ficha-turno" class="d-none">
<!-- Encabezado del turno -->
<div class="d-flex align-items-center gap-3 mb-3">
<div id="ficha-prio-dot" class="prio-dot"
style="width:52px;height:52px;border-radius:12px;display:flex;align-items:center;
justify-content:center;font-size:1.6rem;font-weight:900;color:#fff;background:#3b82f6">
</div>
<div class="flex-grow-1">
<div class="h4 mb-0 fw-bold" id="ficha-codigo">—</div>
<div class="text-muted small" id="ficha-prio-nombre">—</div>
</div>
<button class="btn btn-outline-primary btn-sm" onclick="rellamarActivo()"
title="Volver a llamar este turno (suena en pantalla)">
<i class="fas fa-bell"></i> Re-llamar
</button>
</div>
<!-- ── Sección 1: Paciente ── -->
<!-- ── Número de orden ── -->
<div class="ficha-section" style="padding-bottom:10px;border-bottom:1px solid #f1f5f9;margin-bottom:4px">
<div class="d-flex align-items-center gap-2">
<label style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;white-space:nowrap;margin:0">
<i class="fas fa-hashtag me-1"></i>N.° Orden
</label>
<input type="text" id="inp-consecutivo"
class="form-control form-control-sm"
style="max-width:140px;font-weight:700;font-size:.95rem;letter-spacing:.04em"
placeholder="Cargando…" autocomplete="off">
<button class="btn btn-outline-secondary btn-sm" title="Regenerar siguiente" onclick="recargarConsecutivo()" type="button">
<i class="fas fa-rotate-right" style="font-size:.75rem"></i>
</button>
</div>
</div>
<div class="ficha-section">
<h6><i class="fas fa-user me-1"></i>Paciente</h6>
<!-- Si el kiosko capturó un nombre -->
<div id="bloque-nombre-kiosko" class="d-none mb-2">
<small class="text-muted">Kiosko:</small>
<div class="fw-semibold" id="lbl-nombre-kiosko"></div>
<div id="bloque-pac-kiosko" class="d-none mt-1">
<small class="text-muted">Paciente:</small>
<div class="fw-semibold text-primary" id="lbl-pac-kiosko-nombre"></div>
</div>
</div>
<div id="bloque-pac-no-vinculado">
<div class="input-group mb-2">
<input type="text" id="inp-buscar-pac"
class="form-control form-control-sm"
placeholder="Buscar por nombre o documento…"
autocomplete="off">
<button class="btn btn-outline-secondary btn-sm" onclick="buscarPaciente()" type="button">
<i class="fas fa-search"></i>
</button>
</div>
<div id="lista-pacientes-res"></div>
</div>
<div id="bloque-pac-seleccionado" class="d-none">
<div class="pac-seleccionado">
<div>
<div class="fw-semibold" id="lbl-pac-nombre">—</div>
<div class="text-muted small" id="lbl-pac-doc">—</div>
<div class="text-muted small" id="lbl-pac-cel"></div>
</div>
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm" onclick="abrirModalPaciente(pacienteActivo?.id)" title="Ver / Editar paciente">
<i class="fas fa-pen fa-xs"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="cambiarPaciente()" title="Buscar otro paciente">
<i class="fas fa-exchange-alt fa-xs"></i>
</button>
</div>
</div>
<!-- Historial colapsable del paciente -->
<div id="sec-historial-pac" class="d-none">
<button class="hist-toggle-btn" id="btn-hist-toggle" onclick="toggleHistorialPaciente()">
<i class="fas fa-history" style="color:#6366f1"></i>
Historial del paciente
<i class="fas fa-chevron-down hist-chev"></i>
</button>
<div id="hist-pac-body" class="hist-pac-body d-none">
<div id="hist-pac-content" class="pac-timeline">
<div class="text-center text-muted py-2 small">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando…
</div>
</div>
</div>
</div>
<!-- Embarazo -->
<div class="mt-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="chk-embarazada">
<label class="form-check-label small fw-semibold text-danger" for="chk-embarazada">
<i class="fas fa-baby me-1"></i>Paciente embarazada
</label>
</div>
</div>
<!-- Médico tratante -->
<div class="mt-2" id="bloque-medico">
<div class="small fw-semibold text-secondary mb-1">
<i class="fas fa-user-md me-1"></i>Médico tratante <span class="text-muted fw-normal">(opcional)</span>
</div>
<div id="medico-seleccionado" class="d-none mb-1">
<span class="badge text-bg-light border" style="font-size:.8rem;padding:5px 9px" id="medico-badge-txt"></span>
<button type="button" class="btn btn-link btn-sm p-0 ms-1 text-danger" onclick="quitarMedico()" title="Quitar médico"><i class="fas fa-times"></i></button>
</div>
<div id="medico-buscador">
<input type="text" id="inp-buscar-medico" class="form-control form-control-sm"
placeholder="Buscar por nombre, código o doc…"
autocomplete="off" oninput="buscarMedico()">
<div id="medico-dropdown" class="position-relative">
<ul id="medico-resultados" class="list-unstyled mb-0 border rounded bg-white shadow-sm position-absolute w-100 d-none"
style="z-index:200;max-height:180px;overflow-y:auto;top:2px"></ul>
</div>
</div>
<input type="hidden" id="inp-medico-id">
</div>
</div>
</div>
<!-- ── Sección 2: Lugar destino ── -->
<div class="ficha-section">
<h6><i class="fas fa-map-marker-alt me-1"></i>Lugar destino</h6>
<select id="sel-lugar" class="form-select form-select-sm">
<option value="">— Seleccione destino —</option>
<?php foreach ($lugaresDestino as $ld): ?>
<option value="<?= (int)$ld['id'] ?>"><?= htmlspecialchars($ld['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- ── Sección 3: Exámenes ── -->
<div class="ficha-section">
<h6><i class="fas fa-vial me-1"></i>Exámenes solicitados</h6>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="chk-solo-muestras" onchange="toggleSoloMuestras()">
<label class="form-check-label small fw-semibold text-warning" for="chk-solo-muestras">
<i class="fas fa-vial me-1"></i>Solo entrega de muestras
</label>
</div>
<div id="lista-examenes">
<?php foreach ($examenesAgrupados as $cat => $items): ?>
<div class="exam-group-title"><?= htmlspecialchars($cat) ?></div>
<?php foreach ($items as $ex): ?>
<label class="exam-check-item">
<input type="checkbox" class="exam-chk"
value="<?= (int)$ex['id'] ?>"
data-nombre="<?= htmlspecialchars($ex['nombre']) ?>">
<span><strong><?= htmlspecialchars($ex['codigo']) ?></strong>
— <?= htmlspecialchars($ex['nombre']) ?></span>
</label>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
<div class="mt-2 d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary btn-sm" onclick="toggleTodosExamenes(false)">
Limpiar
</button>
</div>
</div>
<!-- ── Sección 4: Pago ── -->
<div class="ficha-section">
<h6><i class="fas fa-dollar-sign me-1"></i>Cobro (opcional)</h6>
<div class="row g-2">
<div class="col-6">
<input type="number" id="inp-total" class="form-control form-control-sm"
placeholder="Total $" step="100" min="0">
</div>
<div class="col-6">
<select id="sel-pago" class="form-select form-select-sm" onchange="togglePagoCombinado()">
<option value="">— Forma de pago —</option>
<option value="efectivo">Efectivo</option>
<option value="transferencia">Transferencia</option>
<option value="tarjeta">Tarjeta</option>
<option value="eps">EPS / Convenio</option>
<option value="cortesia">Cortesía</option>
<option value="combinado">Pago combinado</option>
</select>
</div>
</div>
<!-- Panel pago combinado -->
<div id="panel-combinado" class="d-none mt-2 p-2 border rounded" style="background:#f8fafc">
<div class="row g-2 align-items-center mb-1">
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-money-bill-wave me-1 text-success"></i>Efectivo</label></div>
<div class="col-7"><input type="number" id="comb-efectivo" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
</div>
<div class="row g-2 align-items-center mb-1">
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-university me-1 text-primary"></i>Transferencia</label></div>
<div class="col-7"><input type="number" id="comb-transferencia" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
</div>
<div class="row g-2 align-items-center">
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-credit-card me-1 text-warning"></i>Tarjeta</label></div>
<div class="col-7"><input type="number" id="comb-tarjeta" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
</div>
</div>
<textarea id="inp-obs" class="form-control form-control-sm mt-2"
rows="2" placeholder="Observaciones…" maxlength="500"></textarea>
</div>
<!-- ── Sección 5: Consentimientos ── -->
<div class="ficha-section" id="sec-consentimientos" style="display:none">
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0"><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
<button class="btn btn-sm btn-outline-primary py-0 px-2"
id="btn-reenviar-consent" onclick="enviarConsentimientosTodos()"
title="Enviar todos por WhatsApp">
<i class="fas fa-paper-plane me-1"></i>Enviar todos
</button>
</div>
<div id="lista-consentimientos"></div>
</div>
<!-- ── Acciones ── -->
<div class="ficha-acciones">
<button class="btn btn-success" id="btn-guardar" onclick="guardarSolicitud()">
<i class="fas fa-save me-1"></i>Guardar solicitud
</button>
<button class="btn btn-primary d-none" id="btn-pasar-lugar" onclick="pasarALugar()">
<i class="fas fa-arrow-right me-1"></i>Pasar a lugar
</button>
<button class="btn btn-warning d-none" id="btn-enviar-consent" onclick="enviarConsentimientos()">
<i class="fas fa-paper-plane me-1"></i>Enviar consentimientos
</button>
<button class="btn btn-outline-secondary" id="btn-soltar" onclick="soltarTurno()" title="Liberar turno para que otro escritorio lo llame">
<i class="fas fa-undo-alt me-1"></i>Soltar
</button>
<button class="btn btn-secondary ms-auto" onclick="marcarAusente()">
<i class="fas fa-user-slash me-1"></i>Ausente
</button>
</div>
</div><!-- /ficha-turno -->
</div><!-- /rec-ficha -->
</div><!-- /rec-layout -->
</main>
<!-- ═══ Modal firma enfermero ═══════════════════════════════════════ -->
<div class="modal fade" id="modalFirmaEnfermero" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title mb-0"><i class="fas fa-pen-nib me-1"></i>Firma del enfermero</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="text-muted small mb-2" id="fe-nombre-consentimiento"></p>
<canvas id="fe-canvas" width="440" height="180"
style="border:1px solid #cbd5e1;border-radius:8px;touch-action:none;cursor:crosshair;max-width:100%"></canvas>
<div class="d-flex justify-content-between mt-2">
<button class="btn btn-sm btn-outline-secondary" onclick="feCanvas.limpiar()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<span class="text-muted small align-self-center">Firme en el recuadro</span>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary btn-sm" onclick="feGuardarFirma()">
<i class="fas fa-check me-1"></i>Guardar firma
</button>
</div>
</div>
</div>
</div>
<!-- ═══ Modal Paciente (crear / ver / editar) ════════════════════════════ -->
<div class="modal fade" id="modalPacienteRec" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius:1rem">
<!-- Header con avatar -->
<div id="mpac-header"
style="background:linear-gradient(135deg,#1043a0 0%,#1565c0 100%);
color:#fff;padding:1.3rem 1.5rem 1rem;
display:flex;align-items:center;gap:.9rem">
<div id="mpac-avatar"
style="width:52px;height:52px;border-radius:50%;
background:rgba(255,255,255,.18);display:flex;
align-items:center;justify-content:center;
font-size:1.4rem;font-weight:800;color:#fff;
border:2px solid rgba(255,255,255,.35);flex-shrink:0">?</div>
<div class="flex-grow-1">
<div style="font-size:1rem;font-weight:700" id="mpac-titulo">Nuevo Paciente</div>
<div style="font-size:.75rem;opacity:.75" id="mpac-subtitulo">Completa los datos del paciente</div>
</div>
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-3 px-4">
<form id="form-paciente-rec" autocomplete="off">
<input type="hidden" id="mpac-id">
<!-- Datos personales -->
<div class="ficha-section mb-3">
<h6><i class="fas fa-id-card me-1"></i>Datos personales</h6>
<div class="row g-2">
<div class="col-12">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-nombre"
required placeholder="Nombre completo"
oninput="mpacAvatar(this.value)">
<label>Nombre completo <span class="text-danger">*</span></label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<select class="form-select form-select-sm" id="mpac-tipo-doc">
<option value="CC">CC — Cédula</option>
<option value="CE">CE — Extranjería</option>
<option value="TI">TI — Tarjeta Identidad</option>
<option value="PA">PA — Pasaporte</option>
<option value="DE">DE — Doc. Extranjero</option>
<option value="NIT">NIT</option>
<option value="RC">RC — Reg. Civil</option>
</select>
<label>Tipo documento</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-doc" placeholder="Número documento">
<label>Número documento</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="date" class="form-control form-control-sm" id="mpac-fnac" placeholder="Fecha nacimiento">
<label>Fecha nacimiento</label>
</div>
</div>
</div>
</div>
<!-- Contacto -->
<div class="ficha-section mb-3">
<h6><i class="fas fa-phone me-1"></i>Contacto</h6>
<div class="row g-2">
<div class="col-sm-5">
<label class="form-label small fw-semibold mb-1" style="color:#64748b">Teléfono</label>
<div class="input-group input-group-sm">
<select id="mpac-tel-prefijo" class="form-select" style="max-width:82px">
<option value="57" selected>🇨🇴 57</option>
<option value="58">🇻🇪 58</option>
<option value="1">🇺🇸 1</option>
</select>
<input type="tel" class="form-control" id="mpac-tel"
placeholder="3001234567" onblur="mpacVerificarWA()">
</div>
<div id="mpac-wa-badge" style="font-size:.78rem;min-height:18px;margin-top:3px"></div>
<input type="hidden" id="mpac-user-id">
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="email" class="form-control form-control-sm" id="mpac-email" placeholder="Email">
<label>Email</label>
</div>
</div>
<div class="col-sm-3">
<div class="form-floating">
<select class="form-select form-select-sm" id="mpac-genero">
<option value="">—</option>
<option value="M">Masculino</option>
<option value="F">Femenino</option>
<option value="O">Otro</option>
</select>
<label>Género</label>
</div>
</div>
</div>
</div>
<!-- Ubicación y EPS -->
<div class="ficha-section mb-2">
<h6><i class="fas fa-map-marker-alt me-1"></i>Ubicación y EPS</h6>
<div class="row g-2">
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-eps" placeholder="EPS">
<label>EPS</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-ciudad" placeholder="Ciudad" value="Cúcuta">
<label>Ciudad</label>
</div>
</div>
<div class="col-sm-4">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-barrio" placeholder="Barrio">
<label>Barrio</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<input type="text" class="form-control form-control-sm" id="mpac-dir" placeholder="Dirección">
<label>Dirección</label>
</div>
</div>
<div class="col-12">
<div class="form-floating">
<textarea class="form-control form-control-sm" id="mpac-notas"
placeholder="Notas internas" style="height:68px"></textarea>
<label>Notas internas</label>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer" style="background:#f8faff;border-top:1px solid #e8edf5;padding:.8rem 1.25rem">
<button type="button" class="btn btn-sm btn-light px-3" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancelar
</button>
<button type="button" class="btn btn-sm btn-primary px-4" id="mpac-btn-guardar" onclick="mpacGuardar()">
<i class="fas fa-save me-1"></i>Guardar paciente
</button>
</div>
</div>
</div>
</div>
<div class="rec-toast" id="rec-toast">
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
<span id="toast-msg"></span>
</div>
<script>
// ── Estado ────────────────────────────────────────────────────
let turnoActivo = null;
let pacienteActivo = null;
let solicitudActiva = null;
let consentimientos = [];
let pollingColaId = null;
const API = '<?= BASE_URL ?>modules/turnero/api/';
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
const BASE_WA = '<?= BASE_URL ?>';
const DESK_ID = <?= $deskId ?: 'null' ?>;
const DESK_NOMBRE = '<?= addslashes($deskNombre ?? 'Recepción') ?>';
// ── Arranque ──────────────────────────────────────────────────
let pollingConsentimientosId = null;
document.addEventListener('DOMContentLoaded', () => {
cargarCola();
pollingColaId = setInterval(cargarCola, 3000);
document.getElementById('inp-buscar-pac')
.addEventListener('keydown', e => { if (e.key === 'Enter') buscarPaciente(); });
document.getElementById('sel-lugar')
.addEventListener('change', onLugarChange);
// Refrescar consentimientos al volver a la pestaña (firma en otra pestaña)
document.addEventListener('visibilitychange', () => {
if (!document.hidden && turnoActivo) refrescarConsentimientosLugar();
});
});
// ── Polling automático de consentimientos cuando hay uno abierto ──
async function refrescarConsentimientosLugar() {
if (!turnoActivo) return;
try {
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoActivo.id}`);
const json = await res.json();
if (json.ok && json.consentimientos) {
consentimientos = json.consentimientos;
renderConsentimientos(consentimientos);
}
} catch (_) {}
}
// ── Formularios del lugar (aparecen al seleccionar lugar) ────
// El dropdown sel-lugar es solo para elegir destino del paciente.
// Los consentimientos del desk se cargan via cargarConsentimientosDesk() y no dependen de este cambio.
function onLugarChange() {}
async function abrirFormularioLugar(formularioId, nombre) {
if (!turnoActivo) { mostrarError('Llama un turno primero.'); return; }
if (!pacienteActivo) { mostrarError('Vincula el paciente antes de abrir el consentimiento.'); return; }
try {
const res = await fetch(API + 'create_consent_token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: formularioId, paciente_id: pacienteActivo?.id || null }),
});
const json = await res.json();
if (json.ok && (json.data?.url || json.url)) {
window.open(json.data?.url || json.url, '_blank');
// Polling automático para detectar la firma
if (!pollingConsentimientosId) {
pollingConsentimientosId = setInterval(refrescarConsentimientosLugar, 3000);
}
} else {
mostrarError(json.error || 'No se pudo generar el enlace de firma.');
}
} catch (err) {
mostrarError('Error al generar enlace: ' + err.message);
}
}
// ── Cola ──────────────────────────────────────────────────────
async function cargarCola() {
try {
const res = await fetch(API + 'get_cola.php?area=recepcion');
const json = await res.json();
if (!json.ok) return;
renderCola(json);
} catch (_) {}
}
function renderCola(snap) {
const lista = document.getElementById('cola-items');
const todos = snap.cola || [];
const badge = document.getElementById('badge-count');
// Filtrar: 'espera' todos; 'en_recepcion' solo los de este escritorio
const visibles = todos.filter(t =>
t.estado === 'espera' ||
(t.estado === 'en_recepcion' && (t.recepcion_desk_id === null || t.recepcion_desk_id == DESK_ID))
);
badge.textContent = visibles.length;
if (visibles.length === 0) {
lista.innerHTML = `<div class="cola-vacia">
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>Cola vacía</div>`;
return;
}
lista.innerHTML = visibles.map(t => `
<div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}" data-id="${t.id}">
<div class="prio-dot" style="background:${t.prioridad_color};cursor:pointer" onclick="seleccionarTurno(${t.id})">${t.prioridad_codigo}</div>
<div class="turno-info" style="cursor:pointer" onclick="seleccionarTurno(${t.id})">
<div class="cod">${escHtml(t.codigo)}</div>
<div class="pac">${escHtml(t.paciente_nombre || 'Sin nombre')}</div>
</div>
<button onclick="llamarTurnoEspecifico(${t.id}, '${t.estado}')"
title="${t.estado === 'en_recepcion' ? 'Re-llamar este turno' : 'Llamar este turno ahora'}"
style="flex-shrink:0;background:#2563eb;border:none;color:#fff;
border-radius:7px;padding:5px 9px;font-size:.78rem;cursor:pointer">
<i class="fas ${t.estado === 'en_recepcion' ? 'fa-volume-up' : 'fa-bell'}"></i>
</button>
</div>`).join('');
}
// ── Llamar siguiente ──────────────────────────────────────────
async function llamarSiguiente() {
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: 'recepcion', desk_id: DESK_ID }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
const turnoLlamado = json.turno ?? json.data?.turno;
if (!turnoLlamado) {
mostrarAviso('Cola vacía', 'No hay turnos en espera.');
resetPlaceholder();
return;
}
mostrarLlamando(turnoLlamado.codigo);
abrirFicha(turnoLlamado);
cargarCola();
} catch (err) {
mostrarError(err.message);
} finally {
btn.disabled = false;
}
}
// ── Seleccionar turno de la cola (ver ficha sin cambiar estado) ─────
async function seleccionarTurno(turnoId) {
try {
const res = await fetch(API + 'get_turno.php?id=' + turnoId);
const json = await res.json();
if (!json.ok) { mostrarError(json.error || 'No se pudo cargar el turno'); return; }
abrirFicha(json.turno ?? json.data?.turno);
} catch (err) {
mostrarError('Error al cargar turno: ' + err.message);
}
}
// ── Llamar turno específico (cambia estado → en_recepcion) ─────────
async function llamarTurnoEspecifico(turnoId, estadoActual) {
// Mostrar feedback inmediato
const card = document.querySelector(`.cola-card[data-id="${turnoId}"]`);
if (card) {
const cod = card.querySelector('.cod')?.textContent || '';
mostrarLlamando(cod);
}
// Si ya está en recepción: re-llamar (actualiza llamado_at para que la pantalla TV reactive)
if (estadoActual === 'en_recepcion') {
try {
const res = await fetch(API + 'rellamar.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoId, desk_id: DESK_ID }),
});
const json = await res.json();
if (json.ok && json.turno) { abrirFicha(json.turno); return; }
} catch (_) {}
// Fallback: solo abrir ficha
seleccionarTurno(turnoId);
return;
}
try {
const res = await fetch(API + 'cambiar_estado.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoId, nuevo_estado: 'en_recepcion' }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
abrirFicha(json.turno);
cargarCola();
} catch (err) {
mostrarError('Error al llamar turno: ' + err.message);
}
}
// ── Re-llamar desde la ficha ────────────────────────────────
async function rellamarActivo() {
if (!turnoActivo) return;
mostrarLlamando(turnoActivo.codigo);
try {
await fetch(API + 'rellamar.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, desk_id: DESK_ID }),
});
} catch (_) {}
}
// ── Abrir ficha ───────────────────────────────────────────────
async function cargarConsecutivo() {
const inp = document.getElementById('inp-consecutivo');
if (!inp) return;
inp.value = '…';
try {
const r = await fetch(API + 'get_consecutivo.php');
const d = await r.json();
if (d.ok) inp.value = d.consecutivo;
} catch(_) { inp.value = ''; }
}
function recargarConsecutivo() { cargarConsecutivo(); }
function abrirFicha(turno) {
turnoActivo = turno;
pacienteActivo = null;
solicitudActiva = null;
document.getElementById('ficha-placeholder').classList.add('d-none');
document.getElementById('ficha-turno').classList.remove('d-none');
cargarConsecutivo();
// Datos del turno
document.getElementById('ficha-codigo').textContent = turno.codigo;
document.getElementById('ficha-prio-nombre').textContent = turno.prioridad_nombre || '';
const dot = document.getElementById('ficha-prio-dot');
dot.textContent = turno.prioridad_codigo || '—';
dot.style.background = turno.prioridad_color || '#3b82f6';
// Badge top
document.getElementById('badge-turno-activo').classList.remove('d-none');
document.getElementById('badge-codigo').textContent = turno.codigo;
// Nombre del kiosko (valor capturado en kiosko)
if (turno.paciente_nombre) {
document.getElementById('bloque-nombre-kiosko').classList.remove('d-none');
document.getElementById('lbl-nombre-kiosko').textContent = turno.paciente_nombre;
document.getElementById('bloque-pac-kiosko').classList.add('d-none');
document.getElementById('inp-buscar-pac').value = turno.paciente_nombre;
}
// Reset secciones
resetCheckboxes();
document.getElementById('sec-consentimientos').style.removeProperty('display');
document.getElementById('sec-consentimientos').style.display = 'none';
document.getElementById('btn-pasar-lugar').classList.add('d-none');
document.getElementById('btn-enviar-consent').classList.add('d-none');
document.getElementById('btn-guardar').classList.remove('d-none');
document.getElementById('btn-guardar').disabled = false;
desvincularPaciente();
// Auto-vincular paciente si ya viene con ID desde el kiosko
if (turno.paciente_id) {
fetch(`${API_PAC}?id=${turno.paciente_id}`)
.then(r => r.json())
.then(json => {
const data = (json.data || json.registros || [])[0];
if (data && !pacienteActivo) seleccionarPaciente(data);
})
.catch(() => {});
}
// Cargar consentimientos del desk actual para este turno
if (DESK_ID) cargarConsentimientosDesk(turno.id);
mostrarFichaMobile();
}
// ── Cargar consentimientos del desk para el turno ─────────────
async function cargarConsentimientosDesk(turnoId) {
const sec = document.getElementById('sec-consentimientos');
const lista = document.getElementById('lista-consentimientos');
try {
// 1. Si el turno ya tiene registros creados, mostrarlos con su estado real
const resExist = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
const jsonExist = await resExist.json();
if (jsonExist.ok && jsonExist.consentimientos?.length) {
consentimientos = jsonExist.consentimientos.map(c => ({
...c,
formulario_nombre: c.formulario_nombre || c.nombre,
}));
renderConsentimientos(consentimientos);
sec.style.display = '';
return;
}
// 2. Aún no existen registros: mostrar los configurados para el desk como pendientes
const resDesk = await fetch(`${API}get_lugar_formularios.php?lugar_id=${DESK_ID}`);
const jsonDesk = await resDesk.json();
if (!jsonDesk.ok || !jsonDesk.formularios?.length) {
sec.style.display = 'none';
return;
}
lista.innerHTML = jsonDesk.formularios.map(f => {
const nom = escHtml(f.nombre);
return `<div class="consent-item pendiente">
<i class="fas fa-file-signature"></i>
<span class="c-nom">${nom}</span>
<span class="c-badge">Pendiente</span>
<div class="consent-acciones">
<button class="btn btn-outline-primary"
onclick="abrirFormularioLugar(${f.id}, '${nom.replace(/'/g, "\\'")}')"
title="Abrir formulario en nueva pestaña">
<i class="fas fa-external-link-alt"></i> Firmar
</button>
</div>
</div>`;
}).join('');
sec.style.display = '';
} catch (_) {
sec.style.display = 'none';
}
}
// ── Buscador de paciente ──────────────────────────────────────
async function buscarPaciente() {
const q = document.getElementById('inp-buscar-pac').value.trim();
if (q.length < 2) return;
try {
const res = await fetch(`${API_PAC}?busqueda=${encodeURIComponent(q)}&limit=8`);
const json = await res.json();
const lista = document.getElementById('lista-pacientes-res');
const datos = json.data || json.registros || [];
if (!datos.length) {
lista.innerHTML = `<div class="text-muted small mt-1">Sin resultados.
<a href="#" onclick="abrirModalPaciente(null,'${escHtml(q).replace(/'/g,"\\'")}');return false">Crear nuevo</a></div>`;
return;
}
lista.innerHTML = datos.map(p => `
<div class="pac-resultado" onclick="seleccionarPaciente(${JSON.stringify(p).replace(/"/g,'&quot;')})">
<div class="pac-nombre">${escHtml(p.nombre_completo||p.full_name||p.nombre||'')}</div>
<div class="pac-doc">${escHtml(p.tipo_documento||'')} ${escHtml(p.documento||p.numero_documento||'')}
${p.telefono||p.celular ? '· ' + escHtml(p.telefono||p.celular) : ''}</div>
</div>`).join('');
} catch (_) {}
}
function seleccionarPaciente(pac) {
pacienteActivo = pac;
// Persistir vínculo en BD para que create_consent_token pueda verificar paciente_id
if (turnoActivo?.id && pac?.id) {
fetch(API + 'vincular_paciente.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, paciente_id: pac.id }),
}).catch(() => {});
}
document.getElementById('bloque-pac-no-vinculado').classList.add('d-none');
document.getElementById('bloque-pac-seleccionado').classList.remove('d-none');
const pacNombre = (pac.full_name || pac.nombre_completo || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
document.getElementById('lbl-pac-nombre').textContent = pacNombre;
document.getElementById('lbl-pac-doc').textContent =
(pac.tipo_documento||'') + ' ' + (pac.documento||pac.numero_documento||'');
document.getElementById('lbl-pac-cel').textContent =
pac.telefono || pac.celular || '';
document.getElementById('lista-pacientes-res').innerHTML = '';
// Mostrar nombre del paciente vinculado debajo del valor del kiosko
if (!document.getElementById('bloque-nombre-kiosko').classList.contains('d-none')) {
document.getElementById('lbl-pac-kiosko-nombre').textContent = pacNombre;
document.getElementById('bloque-pac-kiosko').classList.remove('d-none');
}
// Mostrar historial del paciente
const secHist = document.getElementById('sec-historial-pac');
secHist.classList.remove('d-none');
// Resetear estado colapsado
document.getElementById('hist-pac-body').classList.add('d-none');
document.getElementById('btn-hist-toggle').classList.remove('open');
// Precargar en background (usuario decide si abre)
_historialPacienteCargado = false;
_historialPacienteId = pac.id;
}
function togglePagoCombinado() {
const combinado = document.getElementById('sel-pago').value === 'combinado';
document.getElementById('panel-combinado').classList.toggle('d-none', !combinado);
const inp = document.getElementById('inp-total');
inp.readOnly = combinado;
inp.style.background = combinado ? '#e9ecef' : '';
if (combinado) sumarCombinado();
}
function sumarCombinado() {
const v = id => parseFloat(document.getElementById(id).value) || 0;
const total = v('comb-efectivo') + v('comb-transferencia') + v('comb-tarjeta');
document.getElementById('inp-total').value = total || '';
}
function resetPagoCombinado() {
['comb-efectivo','comb-transferencia','comb-tarjeta'].forEach(id => document.getElementById(id).value = '');
document.getElementById('panel-combinado').classList.add('d-none');
const inp = document.getElementById('inp-total');
inp.readOnly = false;
inp.style.background = '';
}
function desvincularPaciente() {
pacienteActivo = null;
_historialPacienteId = null;
_historialPacienteCargado = false;
document.getElementById('bloque-pac-no-vinculado').classList.remove('d-none');
document.getElementById('bloque-pac-seleccionado').classList.add('d-none');
document.getElementById('bloque-pac-kiosko').classList.add('d-none');
document.getElementById('sec-historial-pac').classList.add('d-none');
document.getElementById('hist-pac-body').classList.add('d-none');
document.getElementById('btn-hist-toggle').classList.remove('open');
document.getElementById('lista-pacientes-res').innerHTML = '';
document.getElementById('inp-buscar-pac').value = '';
document.getElementById('chk-embarazada').checked = false;
}
function cambiarPaciente() {
desvincularPaciente();
setTimeout(() => document.getElementById('inp-buscar-pac').focus(), 80);
}
// ── Historial del paciente ──────────────────────────────────────
let _historialPacienteId = null;
let _historialPacienteCargado = false;
async function toggleHistorialPaciente() {
const body = document.getElementById('hist-pac-body');
const btn = document.getElementById('btn-hist-toggle');
const open = !body.classList.contains('d-none');
body.classList.toggle('d-none', open);
btn.classList.toggle('open', !open);
if (!open && !_historialPacienteCargado && _historialPacienteId) {
_historialPacienteCargado = true;
await cargarHistorialPaciente(_historialPacienteId);
}
}
async function cargarHistorialPaciente(pacienteId) {
const content = document.getElementById('hist-pac-content');
content.innerHTML = '<div class="text-center text-muted py-2 small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando historial…</div>';
try {
const res = await fetch(`${API}get_historial.php?paciente_id=${pacienteId}&per_page=5&page=1`);
const json = await res.json();
if (!json.ok || !json.turnos || !json.turnos.length) {
content.innerHTML = '<div class="text-center text-muted py-3 small"><i class="fas fa-inbox me-1"></i>Sin visitas anteriores registradas</div>';
return;
}
content.innerHTML = renderHistorialTimeline(json.turnos, json.total);
} catch(_) {
content.innerHTML = '<div class="text-center text-danger py-2 small">Error al cargar historial</div>';
}
}
function renderHistorialTimeline(turnos, total) {
const ESTADO_LBL = {
espera:'Espera', en_recepcion:'Recepción', en_espera_lugar:'Esp. lugar',
en_servicio:'En servicio', finalizado:'Finalizado', ausente:'Ausente', cancelado:'Cancelado',
};
const ESTADO_STYLE = {
finalizado: 'background:#f1f5f9;color:#334155',
ausente: 'background:#fee2e2;color:#991b1b',
cancelado: 'background:#f1f5f9;color:#9ca3af',
en_servicio: 'background:#dcfce7;color:#166534',
en_espera_lugar:'background:#fde68a;color:#92400e',
en_recepcion: 'background:#dbeafe;color:#1e40af',
espera: 'background:#fef9c3;color:#78350f',
};
const CONS_ICON = {
pendiente: '📋', enviado: '📤', visto: '👁', firmado: '✅', rechazado: '❌'
};
const totalLabel = total > turnos.length ? `<div class="text-end px-3 pb-1" style="font-size:.67rem;color:#94a3b8">Mostrando ${turnos.length} de ${total} visitas</div>` : '';
const items = turnos.map(t => {
const color = t.prioridad_color || '#6366f1';
const lugar = escHtml(t.lugar_nombre || 'Recepción');
const fecha = t.fecha_sesion || '—';
const est = t.estado || '';
const eStyle = ESTADO_STYLE[est] || 'background:#f1f5f9;color:#334155';
const eLbl = ESTADO_LBL[est] || est;
const mins = t.servicio_min != null ? `<span style="font-size:.65rem;color:#94a3b8;margin-left:4px">${t.servicio_min} min</span>` : '';
// Exámenes
let examStr = '';
if (t.examenes && t.examenes.length) {
examStr = `<div style="margin-top:6px;font-size:.75rem">
<strong style="color:#64748b">Exámenes:</strong>
<div style="color:#475569">${t.examenes.map(e => escHtml(e.nombre)).join(', ')}</div>
</div>`;
}
// Comentarios
let comStr = '';
if (t.comentarios && t.comentarios.length) {
comStr = `<div style="margin-top:6px;font-size:.72rem;border-top:1px solid #e2e8f0;padding-top:6px">
<strong style="color:#64748b">📝 Últimos comentarios:</strong>
<div style="color:#475569;margin-top:4px">${t.comentarios.map(c =>
`<div style="margin-bottom:3px"><strong>${escHtml(c.usuario_nombre)}</strong>: ${escHtml(c.comentario)}</div>`
).join('')}</div>
</div>`;
}
return `<div class="timeline-item">
<div class="timeline-dot" style="color:${color};background:${color}"></div>
<div class="timeline-content">
<div class="timeline-fecha">${escHtml(fecha)}</div>
<div class="timeline-codigo" style="color:${color}">${escHtml(t.codigo)}</div>
<div class="timeline-lugar">${lugar}</div>
<span class="timeline-eb" style="${eStyle}">${escHtml(eLbl)}</span>${mins}
${examStr}${comStr}
</div>
</div>`;
}).join('');
return `<div class="pac-timeline">${items}</div>${totalLabel}`;
}
// ── Guardar solicitud ─────────────────────────────────────────
async function guardarSolicitud() {
if (!turnoActivo) return;
if (!pacienteActivo) { mostrarError('Seleccione un paciente antes de guardar.'); return; }
const lugarId = parseInt(document.getElementById('sel-lugar').value);
if (!lugarId) { mostrarError('Seleccione el lugar destino.'); return; }
const soloMuestras = document.getElementById('chk-solo-muestras').checked;
// ✅ VALIDACIÓN: verificar que los consentimientos del lugar estén firmados (omitir en solo muestras)
if (!soloMuestras) {
const consentPendientes = Array.from(document.querySelectorAll('.consent-item.pendiente, .consent-item.enviado, .consent-item.visto')).length;
if (consentPendientes > 0) {
mostrarError('⚠️ El paciente debe firmar los consentimientos del lugar antes de guardar la solicitud.');
return;
}
}
const examIds = Array.from(document.querySelectorAll('.exam-chk:checked')).map(c => parseInt(c.value));
if (!soloMuestras && !examIds.length) { mostrarError('Seleccione al menos un examen.'); return; }
const btn = document.getElementById('btn-guardar');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const res = await fetch(API + 'create_solicitud.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
turno_id: turnoActivo.id,
paciente_id: pacienteActivo.id,
lugar_id: lugarId,
exam_tipo_ids: examIds,
numero_orden: document.getElementById('inp-consecutivo').value.trim() || null,
total_cobrado: parseFloat(document.getElementById('inp-total').value) || null,
metodo_pago: document.getElementById('sel-pago').value || null,
pagos_detalle: document.getElementById('sel-pago').value === 'combinado' ? {
efectivo: parseFloat(document.getElementById('comb-efectivo').value) || 0,
transferencia: parseFloat(document.getElementById('comb-transferencia').value) || 0,
tarjeta: parseFloat(document.getElementById('comb-tarjeta').value) || 0,
} : null,
observaciones: document.getElementById('inp-obs').value.trim() || null,
embarazada: document.getElementById('chk-embarazada').checked ? 1 : 0,
solo_muestras: soloMuestras ? 1 : 0,
medico_id: parseInt(document.getElementById('inp-medico-id').value) || null,
}),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
solicitudActiva = json.solicitud ?? json.data?.solicitud;
consentimientos = json.consentimientos_requeridos ?? json.data?.consentimientos_requeridos ?? [];
// Asegurar que turnoActivo tiene el estado correcto (en_recepcion)
if (turnoActivo) turnoActivo.estado = 'en_recepcion';
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
// Detener polling de consentimientos (ya se guardó)
if (pollingConsentimientosId) {
clearInterval(pollingConsentimientosId);
pollingConsentimientosId = null;
}
// No mostrar consentimientos post-guardado aquí (ya vienen de create_solicitud)
// Solo renderizar si vienen nuevos
if (consentimientos && consentimientos.length > 0) {
renderConsentimientos(consentimientos);
document.getElementById('sec-consentimientos').style.display = '';
document.getElementById('btn-enviar-consent').classList.remove('d-none');
}
document.getElementById('btn-pasar-lugar').classList.remove('d-none');
document.getElementById('btn-guardar').classList.add('d-none');
} catch (err) {
mostrarError(err.message);
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar solicitud';
}
}
// ── Labels / iconos para estados ─────────────────────────────
const CONSENT_META = {
firmado : { cls:'firmado', ico:'fa-check-circle', lbl:'Firmado' },
enviado : { cls:'enviado', ico:'fa-envelope', lbl:'Enviado' },
visto : { cls:'visto', ico:'fa-eye', lbl:'Visto' },
rechazado : { cls:'rechazado', ico:'fa-ban', lbl:'Rechazado' },
pendiente : { cls:'pendiente', ico:'fa-clock', lbl:'Pendiente' },
};
function renderConsentimientos(lista) {
const el = document.getElementById('lista-consentimientos');
if (!lista.length) {
el.innerHTML = '<div class="text-muted small"><i class="fas fa-check-circle text-success me-1"></i>Sin consentimientos requeridos</div>';
return;
}
el.innerHTML = lista.map(c => {
const m = CONSENT_META[c.estado] || CONSENT_META.pendiente;
const ya = ['firmado','rechazado'].includes(c.estado);
const token = escHtml(c.token || '');
const nom = escHtml(c.formulario_nombre || 'Consentimiento');
const fId = c.formulario_id;
const nomJs = (c.formulario_nombre || 'Consentimiento').replace(/'/g, "\\'");
// Botón de firma paciente (solo si no está firmado/rechazado)
const btnFirmar = (!ya)
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nomJs}')"
title="Abrir firma en nueva pestaña">
<i class="fas fa-external-link-alt"></i> Firmar
</button>` : '';
// Botón ver/enviar WhatsApp
const btnWa = ya
? (c.token ? `<a href="${BASE_WA}ver_formulario_enviado.php?token=${token}" target="_blank"
class="btn btn-outline-secondary" title="Ver formulario firmado">
<i class="fas fa-eye"></i> Ver
</a>` : '')
: `<button class="btn btn-outline-success" onclick="enviarConsentimientoUno(${c.turno_id || turnoActivo?.id})"
title="Enviar enlace de firma por WhatsApp">
<i class="fab fa-whatsapp"></i> WhatsApp
</button>`;
// Fila de firma del profesional (solo si el formulario la requiere y el paciente ya firmó)
let profRow = '';
if (c.estado === 'firmado' && c.requiere_firma_profesional) {
const tienePro = c.tiene_firma_profesional == 1 || c.tiene_firma_profesional === true;
if (tienePro) {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-success"></i>
<span class="text-success">Firma enfermero</span>
<span class="badge bg-success ms-1">OK</span>
</div>`;
} else {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-warning"></i>
<span class="text-warning fw-semibold">Falta firma enfermero</span>
<button class="btn btn-warning btn-sm ms-auto" style="font-size:.7rem;padding:1px 7px"
onclick="abrirFirmaEnfermero(${fId}, '${nomJs}')">
<i class="fas fa-pen-nib me-1"></i>Firmar
</button>
</div>`;
}
}
return `<div class="consent-item ${m.cls}" style="flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem;width:100%">
<i class="fas ${m.ico}"></i>
<span class="c-nom">${nom}</span>
<span class="c-badge">${m.lbl}</span>
<div class="consent-acciones">${btnFirmar}${btnWa}</div>
</div>
${profRow}
</div>`;
}).join('');
}
// ── Firma del enfermero: canvas modal ─────────────────────────
let _feFormularioId = null;
const feCanvas = {
_el: null, _ctx: null, _drawing: false,
init() {
this._el = document.getElementById('fe-canvas');
this._ctx = this._el.getContext('2d');
this._ctx.strokeStyle = '#1e293b';
this._ctx.lineWidth = 2;
this._ctx.lineCap = 'round';
this._el.addEventListener('pointerdown', e => { this._drawing = true; this._ctx.beginPath(); this._move(e); });
this._el.addEventListener('pointermove', e => { if (!this._drawing) return; this._ctx.lineTo(...this._pos(e)); this._ctx.stroke(); });
['pointerup','pointerleave'].forEach(ev => this._el.addEventListener(ev, () => { this._drawing = false; }));
},
_pos(e) { const r = this._el.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top]; },
_move(e) { const [x,y] = this._pos(e); this._ctx.moveTo(x, y); },
limpiar() { this._ctx.clearRect(0, 0, this._el.width, this._el.height); },
vacio() {
const d = this._ctx.getImageData(0, 0, this._el.width, this._el.height).data;
return !d.some(v => v !== 0);
},
png() { return this._el.toDataURL('image/png'); },
};
function abrirFirmaEnfermero(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
_feFormularioId = formularioId;
document.getElementById('fe-nombre-consentimiento').textContent = nombre;
if (!feCanvas._el) feCanvas.init();
feCanvas.limpiar();
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalFirmaEnfermero'));
modal.show();
}
async function feGuardarFirma() {
if (!turnoActivo || !_feFormularioId) return;
if (feCanvas.vacio()) { mostrarError('Dibuja la firma antes de guardar.'); return; }
const png = feCanvas.png();
const btn = document.querySelector('#modalFirmaEnfermero .btn-primary');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const res = await fetch(API + 'firmar_profesional_consentimiento.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: _feFormularioId, svg: png }),
});
const json = await res.json();
if (json.ok) {
bootstrap.Modal.getOrCreateInstance(document.getElementById('modalFirmaEnfermero')).hide();
mostrarToast('Firma del enfermero guardada', 'success');
await refrescarConsentimientosLugar();
} else {
mostrarError(json.error || 'No se pudo guardar la firma.');
}
} catch(e) {
mostrarError('Error al guardar: ' + e.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
}
}
// ── Firmar en nueva pestaña ──────────────────────────────────
async function firmarConsentimiento(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
try {
const res = await fetch(API + 'create_consent_token.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: formularioId, paciente_id: pacienteActivo?.id || null }),
});
const json = await res.json();
if (json.ok && (json.url || json.data?.url)) {
window.open(json.url || json.data.url, '_blank');
// Iniciar polling automático para detectar firma
if (!pollingConsentimientosId) {
pollingConsentimientosId = setInterval(() => {
refrescarConsentimientosLugar();
}, 3000);
}
} else {
mostrarError(json.error || 'No se pudo crear el token de firma');
}
} catch (err) {
mostrarError('Error al abrir firma: ' + err.message);
}
}
function verFirmado(token) {
window.open(BASE_WA + 'ver_formulario_enviado.php?token=' + encodeURIComponent(token), '_blank');
}
// ── Refrescar estado de consentimientos ───────────────────────
async function refrescarConsentimientos(turnoId) {
try {
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
const json = await res.json();
if (json.ok) {
consentimientos = json.consentimientos || [];
renderConsentimientos(consentimientos);
}
} catch(_) {}
}
// ── Enviar consentimientos (todos) ───────────────────────────
async function enviarConsentimientosTodos() {
if (!turnoActivo || !solicitudActiva) return;
const btn = document.getElementById('btn-reenviar-consent');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando…';
try {
const res = await fetch(API + 'send_consentimiento.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
consentimientos = json.consentimientos ?? consentimientos;
renderConsentimientos(consentimientos);
btn.innerHTML = '<i class="fas fa-check me-1"></i>Enviado';
setTimeout(() => { btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar todos'; }, 3000);
} catch (err) {
mostrarError(err.message);
btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar todos';
} finally {
btn.disabled = false;
}
}
// ── Enviar consentimiento individual ─────────────────────────
async function enviarConsentimientoUno(turnoId) {
if (!turnoId) return;
try {
const res = await fetch(API + 'send_consentimiento.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoId }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
consentimientos = json.consentimientos ?? consentimientos;
renderConsentimientos(consentimientos);
} catch (err) {
mostrarError(err.message);
}
}
// ── Enviar consentimientos (legacy — usado por btn-enviar-consent) ──
async function enviarConsentimientos() { return enviarConsentimientosTodos(); }
// ── Pasar a lugar ─────────────────────────────────────────────
async function pasarALugar() {
if (!turnoActivo || !solicitudActiva) return;
const btn = document.getElementById('btn-pasar-lugar');
btn.disabled = true;
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: solicitudActiva.lugar_id,
}),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); btn.disabled = false; return; }
// Reiniciar ficha
turnoActivo = null;
solicitudActiva = null;
document.getElementById('ficha-turno').classList.add('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none');
mostrarToast('Turno pasado al lugar.', 'success', 2000);
setTimeout(() => {
cargarCola();
resetPlaceholder();
}, 500);
} catch (err) {
mostrarError(err.message);
btn.disabled = false;
}
}
// ── Ausente ───────────────────────────────────────────────────
async function marcarAusente() {
if (!turnoActivo) return;
if (!confirm(`¿Marcar turno ${turnoActivo.codigo} como AUSENTE?`)) return;
await fetch(API + 'cambiar_estado.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'ausente' }),
});
turnoActivo = null;
solicitudActiva = null;
document.getElementById('ficha-turno').classList.add('d-none');
document.getElementById('ficha-placeholder').classList.remove('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none');
cargarCola();
volverACola();
}
// ── Soltar turno ──────────────────────────────────────────────
async function soltarTurno() {
if (!turnoActivo) return;
if (!confirm(`¿Soltar turno ${turnoActivo.codigo}? Otro escritorio podrá llamarlo.`)) return;
await fetch(API + 'cambiar_estado.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'espera' }),
});
turnoActivo = null;
solicitudActiva = null;
document.getElementById('ficha-turno').classList.add('d-none');
document.getElementById('ficha-placeholder').classList.remove('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none');
mostrarToast('Turno liberado', 'success');
cargarCola();
volverACola();
}
// ── Helpers ───────────────────────────────────────────────────
// ── Médico tratante ─────────────────────────────────────────────
let _medicoTimer = null;
const API_MEDICOS = '<?= BASE_URL ?>modules/medicos/api/list.php';
function buscarMedico() {
clearTimeout(_medicoTimer);
const q = document.getElementById('inp-buscar-medico').value.trim();
const ul = document.getElementById('medico-resultados');
if (q.length < 2) { ul.classList.add('d-none'); ul.innerHTML = ''; return; }
_medicoTimer = setTimeout(async () => {
const res = await fetch(API_MEDICOS + '?q=' + encodeURIComponent(q));
const json = await res.json();
const rows = (json.data || []).slice(0, 8);
if (!rows.length) { ul.innerHTML = '<li class="px-3 py-2 text-muted small">Sin resultados</li>'; ul.classList.remove('d-none'); return; }
ul.innerHTML = rows.map(m =>
`<li class="px-3 py-2 small" style="cursor:pointer;border-bottom:1px solid #f1f5f9"
onmousedown="seleccionarMedico(${m.id},'${escJs(m.codigo)}','${escJs(m.nombres)} ${escJs(m.apellidos)}','${escJs(m.cod_especialidad||'')}')">
<span class="fw-semibold">${esc(m.nombres)} ${esc(m.apellidos)}</span>
<span class="text-muted ms-1">[${esc(m.codigo)}]</span>
${m.cod_especialidad ? `<span class="badge text-bg-light ms-1" style="font-size:.7rem">${esc(m.cod_especialidad)}</span>` : ''}
</li>`
).join('');
ul.classList.remove('d-none');
}, 250);
}
function seleccionarMedico(id, codigo, nombre, esp) {
document.getElementById('inp-medico-id').value = id;
document.getElementById('medico-badge-txt').textContent = nombre + (esp ? ' · ' + esp : '') + ' [' + codigo + ']';
document.getElementById('medico-seleccionado').classList.remove('d-none');
document.getElementById('medico-buscador').classList.add('d-none');
document.getElementById('medico-resultados').classList.add('d-none');
}
function quitarMedico() {
document.getElementById('inp-medico-id').value = '';
document.getElementById('medico-seleccionado').classList.add('d-none');
document.getElementById('medico-buscador').classList.remove('d-none');
document.getElementById('inp-buscar-medico').value = '';
}
function resetMedico() {
quitarMedico();
}
function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
function toggleSoloMuestras() {
const solo = document.getElementById('chk-solo-muestras').checked;
document.querySelectorAll('.exam-chk').forEach(c => { c.checked = false; c.disabled = solo; });
document.getElementById('lista-examenes').style.opacity = solo ? '0.4' : '';
}
function resetCheckboxes() {
document.getElementById('chk-solo-muestras').checked = false;
document.querySelectorAll('.exam-chk').forEach(c => { c.checked = false; c.disabled = false; });
document.getElementById('lista-examenes').style.opacity = '';
document.getElementById('sel-pago').value = '';
document.getElementById('inp-total').value = '';
document.getElementById('inp-obs').value = '';
resetPagoCombinado();
resetMedico();
}
function toggleTodosExamenes(val) {
document.querySelectorAll('.exam-chk').forEach(c => c.checked = val);
}
function escHtml(str) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(str)));
return d.innerHTML;
}
const esc = escHtml;
function resetPlaceholder() {
const ph = document.getElementById('ficha-placeholder');
ph.innerHTML = `
<i class="fas fa-ticket-alt fa-3x mb-3" style="color:#cbd5e1"></i>
<p class="mb-0 fw-semibold">Sin turno activo</p>
<small class="text-muted">Haga clic en "Llamar siguiente" o en <i class="fas fa-bell"></i> de un turno de la cola</small>`;
volverACola();
}
function esMobile() { return window.innerWidth <= 900; }
function mostrarFichaMobile() {
if (!esMobile()) return;
document.getElementById('rec-ficha').classList.add('mobile-visible');
document.querySelector('.rec-cola').classList.add('mobile-oculta');
}
function volverACola() {
document.getElementById('rec-ficha').classList.remove('mobile-visible');
document.querySelector('.rec-cola').classList.remove('mobile-oculta');
}
function mostrarError(msg) {
mostrarToast(msg, 'error', 4000);
}
function mostrarAviso(titulo, msg) {
mostrarToast(titulo + ': ' + msg, 'warn', 3500);
}
function mostrarLlamando(codigo) {
mostrarToast('Llamando turno ' + codigo + '…', 'info', 2000);
}
/* ── Ocupación exclusiva del escritorio ──────────────────────── */
(function(_ID, _NOMBRE, _VIEW, _PARAM) {
if (!_ID) return;
function _e(s) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(s ?? '')));
return d.innerHTML;
}
function _post(body) {
return fetch(API + 'ocupar_lugar.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({ lugar_id: _ID }, body)),
}).then(r => r.json());
}
function _redirigirSiguiente(disponibles) {
if (disponibles && disponibles.length) {
window.location.href = BASE_WA + 'erp.php?m=turnero&v=' + _VIEW + '&' + _PARAM + '=' + disponibles[0].id;
} else {
window.location.href = BASE_WA + 'erp.php?m=turnero';
}
}
/* Pantalla de bloqueo */
function _mostrarBloqueado(ocupadoPor) {
document.body.innerHTML =
'<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
'min-height:100vh;background:#f8faff;font-family:sans-serif;gap:14px;text-align:center;padding:28px">' +
'<div style="font-size:4rem">🔒</div>' +
'<h2 style="color:#1565c0;margin:0">' + _e(_NOMBRE) + ' está abierta en otro equipo</h2>' +
'<p style="color:#555;font-size:1.05rem;max-width:440px;line-height:1.6">' +
'Actualmente usada por <strong>' + _e(ocupadoPor) + '</strong>.<br>' +
'Puedes solicitar que cierre su sesión o ir al siguiente escritorio disponible.</p>' +
'<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:4px">' +
'<button id="_btn-solicitar" onclick="window._ocToma()" ' +
'style="padding:12px 26px;background:#1565c0;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
'📢 Solicitar cierre en ese equipo</button>' +
'<button id="_btn-siguiente" onclick="window._ocSiguiente()" ' +
'style="padding:12px 26px;background:#f0f4ff;color:#1565c0;border:2px solid #1565c0;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
'➡ Ir al siguiente disponible</button>' +
'</div>' +
'<div id="_oc-estado" style="min-height:24px;color:#777;font-size:.95rem;margin-top:4px"></div>' +
'</div>';
}
function _setEstado(msg, color) {
const el = document.getElementById('_oc-estado');
if (el) el.innerHTML = '<span style="color:' + (color||'#555') + '">' + msg + '</span>';
}
/* Solicitar toma de control */
let _checkTransferTimer = null;
window._ocToma = async function() {
const btn = document.getElementById('_btn-solicitar');
if (btn) btn.disabled = true;
_setEstado('Enviando solicitud…', '#555');
try {
const j = await _post({ action: 'solicitar' });
if (!j.ok) { _setEstado('Error al enviar solicitud.', '#c62828'); if (btn) btn.disabled = false; return; }
if (j.transferido) { location.reload(); return; }
_setEstado('✉ Solicitud enviada. El otro equipo tiene 5 segundos para responder…', '#1565c0');
clearInterval(_checkTransferTimer);
_checkTransferTimer = setInterval(async () => {
try {
const r = await _post({ action: 'check_transfer' });
if (r.disponible) {
clearInterval(_checkTransferTimer);
_setEstado('✅ Escritorio liberado. Tomando control…', '#2e7d32');
const rc = await _post({ action: 'claim' });
if (rc.disponible) location.reload();
else _setEstado('No se pudo tomar el control. Recargue la página.', '#c62828');
} else if (r.denegado) {
clearInterval(_checkTransferTimer);
_setEstado('❌ La solicitud fue denegada.', '#c62828');
if (btn) { btn.disabled = false; btn.textContent = '📢 Volver a solicitar'; }
}
} catch(_) {}
}, 1500);
} catch(e) {
_setEstado('Error de red: ' + e.message, '#c62828');
if (btn) btn.disabled = false;
}
};
/* Ir al siguiente disponible */
window._ocSiguiente = async function() {
const btn = document.getElementById('_btn-siguiente');
if (btn) btn.disabled = true;
_setEstado('Buscando escritorio disponible…', '#555');
try {
const j = await _post({ action: 'disponibles' });
if (!j.ok || !j.disponibles.length) {
_setEstado('No hay otros escritorios disponibles.', '#c62828');
if (btn) btn.disabled = false;
return;
}
_redirigirSiguiente(j.disponibles);
} catch(e) {
_setEstado('Error de red.', '#c62828');
if (btn) btn.disabled = false;
}
};
/* Modal de solicitud entrante (para el ocupante actual) */
let _modalActivo = false;
function _mostrarModalSolicitud(solicitante) {
if (_modalActivo) return;
_modalActivo = true;
const modal = document.createElement('div');
modal.style.cssText =
'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center';
modal.innerHTML =
'<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:92%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.25)">' +
'<div style="font-size:2.5rem">⚠️</div>' +
'<h3 style="color:#c62828;margin:10px 0 6px">Solicitud de transferencia</h3>' +
'<p style="color:#333;margin:0 0 6px"><strong>' + _e(solicitante) + '</strong> quiere usar este escritorio.</p>' +
'<p style="color:#555;font-size:.95rem">Si no responde, se cerrará automáticamente en <strong id="_sol-cnt">5</strong> segundos.</p>' +
'<div style="display:flex;gap:10px;justify-content:center;margin-top:18px">' +
'<button id="_sol-aceptar" style="padding:10px 22px;background:#c62828;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">Aceptar y ceder</button>' +
'<button id="_sol-denegar" style="padding:10px 22px;background:#f5f5f5;color:#333;border:1px solid #ccc;border-radius:8px;font-size:1rem;cursor:pointer">Denegar</button>' +
'</div></div>';
document.body.appendChild(modal);
let secs = 5;
const tick = setInterval(() => {
secs--;
const el = document.getElementById('_sol-cnt');
if (el) el.textContent = secs;
if (secs <= 0) { clearInterval(tick); _liberar(); }
}, 1000);
async function _liberar() {
modal.remove();
await _post({ action: 'aceptar' }).catch(() => {});
try {
const j = await _post({ action: 'disponibles' });
_redirigirSiguiente(j.disponibles || []);
} catch(_) {
window.location.href = BASE_WA + 'erp.php?m=turnero';
}
}
document.getElementById('_sol-aceptar').onclick = () => { clearInterval(tick); _liberar(); };
document.getElementById('_sol-denegar').onclick = async () => {
clearInterval(tick);
modal.remove();
_modalActivo = false;
await _post({ action: 'denegar' }).catch(() => {});
};
}
/* Arranque */
async function _iniciar() {
try {
const j = await _post({ action: 'claim' });
if (!j.ok) return;
if (!j.disponible) { _mostrarBloqueado(j.ocupado_por); return; }
// Heartbeat cada 60 s
setInterval(() => _post({ action: 'ping' }).catch(() => {}), 60000);
// Polling de solicitudes entrantes cada 2 s
setInterval(async () => {
if (_modalActivo) return;
try {
const r = await _post({ action: 'check_solicitud' });
if (r.hay_solicitud) _mostrarModalSolicitud(r.solicitante);
} catch(_) {}
}, 2000);
} catch(_) {}
}
window.addEventListener('beforeunload', () => {
navigator.sendBeacon(API + 'ocupar_lugar.php',
new Blob([JSON.stringify({ lugar_id: _ID, action: 'release' })], { type: 'application/json' }));
});
_iniciar();
})(DESK_ID, DESK_NOMBRE, 'recepcion', 'desk_id');
// ── Modal paciente (crear / ver / editar) ────────────────────
const _mpacModal = () => bootstrap.Modal.getOrCreateInstance(document.getElementById('modalPacienteRec'));
function mpacAvatar(nombre) {
const parts = nombre.trim().split(/\s+/).filter(Boolean);
document.getElementById('mpac-avatar').textContent = parts.length >= 2
? (parts[0][0] + parts[1][0]).toUpperCase()
: (parts[0]?.[0] || '?').toUpperCase();
}
async function abrirModalPaciente(id, nombrePrefill) {
// Reset form
document.getElementById('form-paciente-rec').reset();
document.getElementById('mpac-id').value = '';
document.getElementById('mpac-avatar').textContent = '?';
document.getElementById('mpac-wa-badge').innerHTML = '';
document.getElementById('mpac-user-id').value = '';
document.getElementById('mpac-ciudad').value = 'Cúcuta';
if (id) {
document.getElementById('mpac-titulo').textContent = 'Cargando…';
document.getElementById('mpac-subtitulo').textContent = '';
_mpacModal().show();
try {
const r = await fetch(`${API_PAC}?id=${id}`);
const d = await r.json();
const p = (d.data || d.registros || [])[0];
if (!p) { mostrarError('No se pudo cargar el paciente.'); return; }
mpacRellenar(p);
} catch (_) { mostrarError('Error al cargar paciente.'); }
} else {
document.getElementById('mpac-titulo').textContent = 'Nuevo Paciente';
document.getElementById('mpac-subtitulo').textContent = 'Completa los datos del paciente';
if (nombrePrefill) {
document.getElementById('mpac-nombre').value = nombrePrefill;
mpacAvatar(nombrePrefill);
}
_mpacModal().show();
setTimeout(() => document.getElementById('mpac-nombre').focus(), 300);
}
}
function mpacRellenar(p) {
document.getElementById('mpac-id').value = p.id;
document.getElementById('mpac-nombre').value = p.nombre_completo || '';
document.getElementById('mpac-doc').value = p.numero_documento || '';
document.getElementById('mpac-tipo-doc').value = p.tipo_documento || 'CC';
document.getElementById('mpac-fnac').value = (p.fecha_nacimiento || '').slice(0, 10);
document.getElementById('mpac-email').value = p.email || '';
document.getElementById('mpac-genero').value = p.genero || '';
document.getElementById('mpac-eps').value = p.eps || '';
document.getElementById('mpac-ciudad').value = p.ciudad || 'Cúcuta';
document.getElementById('mpac-barrio').value = p.barrio || '';
document.getElementById('mpac-dir').value = p.direccion || '';
document.getElementById('mpac-notas').value = p.notas_admin || '';
// Teléfono con prefijo
let tel = p.telefono || '';
const prefijos = ['57','58','1'];
let prefijo = '57';
for (const pf of prefijos) {
if (tel.startsWith(pf) && tel.length > pf.length) {
prefijo = pf; tel = tel.slice(pf.length); break;
}
}
document.getElementById('mpac-tel-prefijo').value = prefijo;
document.getElementById('mpac-tel').value = tel;
mpacAvatar(p.nombre_completo || '');
document.getElementById('mpac-titulo').textContent = 'Editar Paciente';
document.getElementById('mpac-subtitulo').textContent = p.nombre_completo || '';
}
async function mpacVerificarWA() {
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
let tel = (document.getElementById('mpac-tel').value || '').replace(/[^0-9]/g, '');
if (tel.length === 10) tel = prefijo + tel;
const badge = document.getElementById('mpac-wa-badge');
if (!tel || tel.length < 10) { badge.innerHTML = ''; return; }
badge.innerHTML = '<span class="text-muted"><i class="fas fa-spinner fa-spin me-1"></i>Verificando…</span>';
try {
const r = await fetch(`<?= BASE_URL ?>api/lab/check_whatsapp.php?phone=${encodeURIComponent(tel)}`);
const d = await r.json();
if (d.ok && d.found) {
badge.innerHTML = '<span class="text-success"><i class="fab fa-whatsapp me-1"></i>Registrado en WhatsApp</span>';
document.getElementById('mpac-user-id').value = d.user_id;
} else {
badge.innerHTML = '<span class="text-muted"><i class="fas fa-times-circle me-1"></i>Sin WhatsApp registrado</span>';
document.getElementById('mpac-user-id').value = '';
}
} catch(_) { badge.innerHTML = ''; }
}
async function mpacGuardar() {
const nombre = document.getElementById('mpac-nombre').value.trim();
if (!nombre) { mostrarError('El nombre es obligatorio.'); return; }
if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) {
mostrarError('El nombre solo debe contener letras, tildes y espacios.'); return;
}
if (nombre.split(/\s+/).filter(Boolean).length < 2) {
mostrarError('Ingresa nombre y apellido (mínimo 2 palabras).'); return;
}
const tipoDoc = document.getElementById('mpac-tipo-doc').value;
const docVal = document.getElementById('mpac-doc').value.trim();
if (docVal && ['CC','TI','RC','CE'].includes(tipoDoc)) {
const digits = docVal.replace(/[^0-9]/g, '');
if (digits.length < 4 || digits.length > 12) {
mostrarError('El documento debe tener entre 4 y 12 dígitos.'); return;
}
}
const emailVal = document.getElementById('mpac-email').value.trim();
if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) {
mostrarError('El correo electrónico no es válido.'); return;
}
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
let tel = document.getElementById('mpac-tel').value.replace(/[^0-9]/g, '');
if (tel.length === 10) tel = prefijo + tel;
if (tel && prefijo === '57') {
const sinPref = tel.replace(/^57/, '');
if (!/^3\d{9}$/.test(sinPref)) {
mostrarError('El celular colombiano debe comenzar por 3 y tener 10 dígitos.'); return;
}
}
const id = document.getElementById('mpac-id').value;
const datos = {
nombre_completo: nombre,
tipo_documento: tipoDoc,
numero_documento: docVal || null,
fecha_nacimiento: document.getElementById('mpac-fnac').value || null,
telefono: tel || null,
email: emailVal || null,
genero: document.getElementById('mpac-genero').value || null,
eps: document.getElementById('mpac-eps').value.trim() || null,
ciudad: document.getElementById('mpac-ciudad').value.trim() || null,
barrio: document.getElementById('mpac-barrio').value.trim() || null,
direccion: document.getElementById('mpac-dir').value.trim() || null,
notas_admin: document.getElementById('mpac-notas').value.trim() || null,
user_id: document.getElementById('mpac-user-id').value || null,
};
if (id) datos.id = parseInt(id);
const btn = document.getElementById('mpac-btn-guardar');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const r = await fetch('<?= BASE_URL ?>api/lab/save_paciente.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
});
const d = await r.json();
if (!d.success && !d.ok) {
mostrarError(d.error || 'Error al guardar el paciente.');
return;
}
_mpacModal().hide();
mostrarToast(d.message || 'Paciente guardado.', 'success');
if (id) {
// Edición: actualizar datos en pantalla si es el paciente activo
if (pacienteActivo && pacienteActivo.id == id) {
pacienteActivo.nombre_completo = nombre;
pacienteActivo.numero_documento = docVal;
pacienteActivo.tipo_documento = tipoDoc;
pacienteActivo.telefono = tel;
document.getElementById('lbl-pac-nombre').textContent =
(pacienteActivo.full_name || nombre).trim();
document.getElementById('lbl-pac-doc').textContent =
tipoDoc + ' ' + (docVal || '');
document.getElementById('lbl-pac-cel').textContent = tel || '';
}
} else {
// Creación: vincular al turno automáticamente
const nuevoPac = {
id: d.id || d.data?.id,
nombre_completo: nombre,
full_name: nombre,
tipo_documento: tipoDoc,
numero_documento: docVal,
telefono: tel,
};
seleccionarPaciente(nuevoPac);
}
} catch (err) {
mostrarError('Error de red: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar paciente';
}
}
// ── Toast ────────────────────────────────────────────────────
let toastTimer = null;
function mostrarToast(msg, type = 'info', duration = 2500) {
const el = document.getElementById('rec-toast');
const ico = document.getElementById('toast-ico');
const txt = document.getElementById('toast-msg');
const icons = { success: 'fa-check-circle', info: 'fa-info-circle', warn: 'fa-exclamation-triangle', error: 'fa-times-circle' };
el.className = 'rec-toast ' + type;
ico.innerHTML = '<i class="fas ' + (icons[type] || icons.info) + '"></i>';
txt.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), duration);
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
</body>
</html>