up
This commit is contained in:
@@ -0,0 +1,760 @@
|
||||
<?php
|
||||
/**
|
||||
* Puesto Lugar / Estación de Servicio — Módulo Turnero
|
||||
* Genérica: sirve para Toma de Muestras 1, 2, Rayos X, etc.
|
||||
* Requiere login + módulo turnero.
|
||||
*
|
||||
* URL: /modules/turnero/views/lugar.php?lugar_id=X
|
||||
* Si no se pasa lugar_id, muestra un selector al entrar.
|
||||
*/
|
||||
require_once __DIR__ . '/../../../config/config.php';
|
||||
if (!isUserLoggedIn()) {
|
||||
header('Location: ' . BASE_URL . 'login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cargar lista de lugares activos
|
||||
try {
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$lugares = $pdo->query(
|
||||
"SELECT id, nombre, descripcion 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;
|
||||
|
||||
// Resolver nombre del lugar para el título
|
||||
$lugarNombre = 'Estación de Servicio';
|
||||
foreach ($lugares as $l) {
|
||||
if ((int)$l['id'] === $lugarIdParam) {
|
||||
$lugarNombre = $l['nombre'];
|
||||
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: 300px 1fr;
|
||||
height: calc(100vh - 52px);
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.lugar-layout { grid-template-columns: 1fr; }
|
||||
.lugar-cola { max-height: 240px; }
|
||||
}
|
||||
|
||||
/* ── Columna cola ── */
|
||||
.lugar-cola {
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.lugar-cola-hdr {
|
||||
padding: .85rem 1rem .5rem;
|
||||
background: #fff; border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cola-items { flex: 1; overflow-y: auto; padding: .4rem; }
|
||||
.cola-card {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
|
||||
padding: .55rem .8rem; margin-bottom: .35rem;
|
||||
display: flex; align-items: center; gap: .55rem;
|
||||
}
|
||||
.cola-card.activo { border-color: #6366f1; box-shadow: 0 0 0 2px #c7d2fe; }
|
||||
.prio-dot {
|
||||
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1rem; font-weight: 800; color: #fff;
|
||||
}
|
||||
.turno-info .cod { font-size: .98rem; font-weight: 700; }
|
||||
.turno-info .pac { font-size: .75rem; color: #64748b;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cola-vacia { text-align: center; padding: 2.5rem 1rem; color: #94a3b8; font-size: .88rem; }
|
||||
|
||||
/* ── Columna ficha ── */
|
||||
.lugar-ficha { overflow-y: auto; padding: 1.2rem 1.5rem; background: #fff; }
|
||||
.ficha-placeholder {
|
||||
height: 100%; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
color: #94a3b8; text-align: center;
|
||||
}
|
||||
|
||||
/* ── Secciones ── */
|
||||
.ficha-sec {
|
||||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||||
border-radius: 12px; padding: 1.1rem 1.2rem; margin-bottom: 1rem;
|
||||
}
|
||||
.ficha-sec h6 {
|
||||
font-size: .75rem; text-transform: uppercase; letter-spacing: .08em;
|
||||
color: #475569; margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
/* ── Datos paciente ── */
|
||||
.pac-dato { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .3rem; }
|
||||
.pac-dato .lbl { font-size: .75rem; color: #94a3b8; min-width: 80px; }
|
||||
.pac-dato .val { font-size: .88rem; 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: .2rem .65rem; font-size: .78rem; font-weight: 600;
|
||||
margin: .15rem;
|
||||
}
|
||||
|
||||
/* ── Consentimientos ── */
|
||||
.consent-row {
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
padding: .5rem .65rem; border-radius: 8px; margin-bottom: .35rem;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.consent-row.firmado { background: #f0fdf4; color: #166534; }
|
||||
.consent-row.rechazado { background: #f1f5f9; color: #475569; }
|
||||
.consent-row.enviado { background: #eff6ff; color: #1e40af; }
|
||||
.consent-row.pendiente { background: #fffbeb; color: #92400e; }
|
||||
.consent-row.visto { background: #faf5ff; color: #6b21a8; }
|
||||
.consent-row .nom-form { flex: 1; }
|
||||
.consent-row .acciones-consent { display: flex; gap: .4rem; }
|
||||
|
||||
/* ── Barra de acciones ── */
|
||||
.ficha-acciones {
|
||||
position: sticky; bottom: 0;
|
||||
background: #fff; border-top: 1px solid #e2e8f0;
|
||||
padding: .85rem 0 .2rem; margin-top: 1rem;
|
||||
display: flex; gap: .5rem; flex-wrap: wrap; align-items: center;
|
||||
}
|
||||
|
||||
/* ── Modal de firma ── */
|
||||
.modal-firma-backdrop {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||
z-index: 1050; display: none; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-firma-backdrop.open { display: flex; }
|
||||
.modal-firma-box {
|
||||
background: #fff; border-radius: 16px;
|
||||
width: min(96vw, 800px); height: min(90vh, 700px);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,.35);
|
||||
}
|
||||
.modal-firma-hdr {
|
||||
padding: .75rem 1rem; border-bottom: 1px solid #e2e8f0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-firma-box iframe {
|
||||
flex: 1; border: none; width: 100%;
|
||||
}
|
||||
|
||||
/* ── Selector de lugar (pantalla inicial) ── */
|
||||
.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; }
|
||||
|
||||
/* ── Indicador de bloqueo ── */
|
||||
.bloqueo-banner {
|
||||
background: #fef2f2; border: 1px solid #fca5a5; border-radius: 10px;
|
||||
padding: .65rem 1rem; font-size: .85rem; color: #991b1b;
|
||||
display: flex; align-items: center; gap: .5rem; margin-bottom: .75rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php
|
||||
$SIDEBAR_TITLE = 'Turnero';
|
||||
$SIDEBAR_ICON = 'fas fa-ticket-alt';
|
||||
require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
||||
?>
|
||||
|
||||
<!-- ══ Selector inicial de lugar (si no viene en URL) ════════ -->
|
||||
<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="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" id="badge-cod-activo"></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cambiarLugar()">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Cambiar lugar
|
||||
</button>
|
||||
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
||||
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
||||
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lugar-layout">
|
||||
|
||||
<!-- ══ Cola ═══════════════════════════════════════════ -->
|
||||
<div class="lugar-cola">
|
||||
<div class="lugar-cola-hdr">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="fw-semibold text-secondary" style="font-size:.78rem;text-transform:uppercase;letter-spacing:.05em">
|
||||
<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>
|
||||
|
||||
<!-- ══ Ficha del turno ════════════════════════════════ -->
|
||||
<div class="lugar-ficha" id="lugar-ficha">
|
||||
|
||||
<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 espere a que llegue el turno</small>
|
||||
</div>
|
||||
|
||||
<div id="ficha-turno" class="d-none">
|
||||
|
||||
<!-- Encabezado 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;font-size:1.6rem;font-weight:900">—</div>
|
||||
<div>
|
||||
<div class="h4 mb-0 fw-bold" id="ficha-codigo">—</div>
|
||||
<div class="text-muted small" id="ficha-prio-nombre"></div>
|
||||
</div>
|
||||
<span class="ms-auto badge bg-success-subtle text-success border border-success-subtle"
|
||||
id="ficha-estado-badge">en espera</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Paciente ── -->
|
||||
<div class="ficha-sec">
|
||||
<h6><i class="fas fa-user me-1"></i>Paciente</h6>
|
||||
<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="bloque-pac-sin" class="text-muted small">
|
||||
<i class="fas fa-info-circle me-1"></i>Sin paciente vinculado
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Exámenes ── -->
|
||||
<div class="ficha-sec">
|
||||
<h6><i class="fas fa-vial me-1"></i>Exámenes solicitados</h6>
|
||||
<div id="lista-examenes-ficha">
|
||||
<span class="text-muted small">Sin exámenes registrados</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sección: Consentimientos ── -->
|
||||
<div class="ficha-sec" id="sec-consent">
|
||||
<h6><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
|
||||
|
||||
<div id="bloqueo-banner" class="bloqueo-banner d-none">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>Hay consentimientos pendientes. No puede iniciar la atención 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>
|
||||
|
||||
<!-- ── Barra de acciones ── -->
|
||||
<div class="ficha-acciones">
|
||||
<button class="btn btn-success" id="btn-iniciar" onclick="iniciarAtencion()">
|
||||
<i class="fas fa-play me-1"></i>Iniciar atención
|
||||
</button>
|
||||
<button class="btn btn-primary d-none" id="btn-finalizar" onclick="finalizarAtencion()">
|
||||
<i class="fas fa-flag-checkered me-1"></i>Finalizar
|
||||
</button>
|
||||
<button class="btn btn-outline-warning d-none" id="btn-regresar" onclick="regresarCola()">
|
||||
<i class="fas fa-undo me-1"></i>Regresar a cola
|
||||
</button>
|
||||
<button class="btn btn-secondary ms-auto" id="btn-ausente" onclick="marcarAusente()">
|
||||
<i class="fas fa-user-slash me-1"></i>Ausente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /ficha-turno -->
|
||||
</div><!-- /lugar-ficha -->
|
||||
|
||||
</div><!-- /lugar-layout -->
|
||||
</main>
|
||||
|
||||
<!-- ══ Modal de firma presencial ══════════════════════════════ -->
|
||||
<div class="modal-firma-backdrop" id="modal-firma">
|
||||
<div class="modal-firma-box">
|
||||
<div class="modal-firma-hdr">
|
||||
<strong id="modal-firma-titulo">Firmar consentimiento</strong>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cerrarModalFirma()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<iframe id="firma-iframe" src="" title="Formulario de consentimiento"></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;
|
||||
|
||||
const API = '../api/';
|
||||
const BASE_WA = '<?= BASE_URL ?>';
|
||||
|
||||
// ── Arranque ──────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (lugarId) {
|
||||
iniciarPuesto();
|
||||
}
|
||||
});
|
||||
|
||||
function iniciarPuesto() {
|
||||
cargarCola();
|
||||
pollingColaId = setInterval(cargarCola, 7000);
|
||||
}
|
||||
|
||||
// ── Selector de lugar ─────────────────────────────────────────
|
||||
function confirmarLugar() {
|
||||
const sel = document.getElementById('sel-lugar-init');
|
||||
const id = parseInt(sel.value);
|
||||
if (!id) return alert('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.data);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function renderCola(snap) {
|
||||
const espera = (snap.cola || []).filter(t =>
|
||||
t.estado === 'en_espera_lugar'
|
||||
);
|
||||
document.getElementById('badge-count').textContent = espera.length;
|
||||
|
||||
const lista = document.getElementById('cola-items');
|
||||
if (!espera.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;
|
||||
}
|
||||
lista.innerHTML = espera.map(t => `
|
||||
<div class="cola-card ${turnoActivo?.id === t.id ? 'activo' : ''}">
|
||||
<div class="prio-dot" style="background:${t.prioridad_color}">${t.prioridad_codigo}</div>
|
||||
<div class="turno-info">
|
||||
<div class="cod">${escHtml(t.codigo)}</div>
|
||||
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Llamar siguiente ──────────────────────────────────────────
|
||||
async function llamarSiguiente() {
|
||||
if (!lugarId) { alert('Primero seleccione un lugar.'); return; }
|
||||
const btn = document.getElementById('btn-llamar');
|
||||
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 }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { alert('Error: ' + json.error); return; }
|
||||
|
||||
if (!json.data.turno) {
|
||||
alert('Cola del lugar vacía.');
|
||||
return;
|
||||
}
|
||||
await abrirFicha(json.data.turno);
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Abrir ficha ───────────────────────────────────────────────
|
||||
async function abrirFicha(turno) {
|
||||
turnoActivo = turno;
|
||||
|
||||
document.getElementById('ficha-placeholder').classList.add('d-none');
|
||||
document.getElementById('ficha-turno').classList.remove('d-none');
|
||||
|
||||
// Header
|
||||
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');
|
||||
|
||||
actualizarEstadoBadge(turno.estado);
|
||||
|
||||
// Cargar ficha del paciente/exámenes desde la solicitud
|
||||
await cargarFichaSolicitud(turno.id);
|
||||
|
||||
// Iniciar polling de consentimientos
|
||||
clearInterval(pollingConsentId);
|
||||
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
|
||||
}
|
||||
|
||||
// ── Cargar datos de la solicitud del turno ────────────────────
|
||||
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.data.solicitud;
|
||||
const pac = json.data.paciente;
|
||||
const exams = json.data.examenes || [];
|
||||
const consts = json.data.consentimientos || [];
|
||||
|
||||
// Datos del paciente
|
||||
if (pac) {
|
||||
document.getElementById('bloque-pac-info').style.display = '';
|
||||
document.getElementById('bloque-pac-sin').classList.add('d-none');
|
||||
document.getElementById('pac-nombre').textContent =
|
||||
(pac.full_name || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim() || '—';
|
||||
document.getElementById('pac-doc').textContent =
|
||||
(pac.tipo_documento||'') + ' ' + (pac.documento||'') || '—';
|
||||
document.getElementById('pac-fec').textContent = pac.fecha_nacimiento || '—';
|
||||
document.getElementById('pac-cel').textContent = pac.celular || pac.telefono || '—';
|
||||
} else {
|
||||
document.getElementById('bloque-pac-info').style.display = 'none';
|
||||
document.getElementById('bloque-pac-sin').classList.remove('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>';
|
||||
}
|
||||
|
||||
// Consentimientos
|
||||
renderConsentimientos(consts);
|
||||
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Consentimientos ───────────────────────────────────────────
|
||||
function renderConsentimientos(lista) {
|
||||
tieneConsent = lista.length > 0;
|
||||
hayPendientes = lista.some(c => !['firmado', 'rechazado'].includes(c.estado));
|
||||
|
||||
const sec = document.getElementById('sec-consent');
|
||||
const listEl = document.getElementById('lista-consent');
|
||||
const sinEl = document.getElementById('sin-consent');
|
||||
const banner = document.getElementById('bloqueo-banner');
|
||||
const btnIni = document.getElementById('btn-iniciar');
|
||||
|
||||
if (!tieneConsent) {
|
||||
listEl.innerHTML = '';
|
||||
sinEl.classList.remove('d-none');
|
||||
banner.classList.add('d-none');
|
||||
btnIni.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sinEl.classList.add('d-none');
|
||||
banner.classList.toggle('d-none', !hayPendientes);
|
||||
btnIni.disabled = hayPendientes;
|
||||
|
||||
listEl.innerHTML = lista.map(c => {
|
||||
const cls = c.estado;
|
||||
const ico = c.estado === 'firmado' ? 'fa-check-circle'
|
||||
: c.estado === 'rechazado' ? 'fa-ban'
|
||||
: c.estado === 'enviado' ? 'fa-envelope'
|
||||
: c.estado === 'visto' ? 'fa-eye'
|
||||
: 'fa-clock';
|
||||
const label = c.estado === 'firmado' ? 'Firmado'
|
||||
: c.estado === 'rechazado' ? 'Rechazado'
|
||||
: c.estado === 'enviado' ? 'Enviado'
|
||||
: c.estado === 'visto' ? 'Visto'
|
||||
: 'Pendiente';
|
||||
|
||||
const botonesAccion = !['firmado','rechazado'].includes(c.estado) ? `
|
||||
<div class="acciones-consent">
|
||||
<button class="btn btn-outline-secondary btn-sm py-0" title="Reenviar WhatsApp"
|
||||
onclick="reenviarConsentimiento(${c.turno_id})">
|
||||
<i class="fas fa-paper-plane" style="font-size:.75rem"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm py-0" title="Firmar presencialmente"
|
||||
onclick="abrirFirmaPresencial('${c.token}', ${c.id}, ${JSON.stringify(c.formulario_nombre).replace(/"/g,'"')})">
|
||||
<i class="fas fa-signature" style="font-size:.75rem"></i> Firmar aquí
|
||||
</button>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="consent-row ${cls}" data-consent-id="${c.id}">
|
||||
<i class="fas ${ico}"></i>
|
||||
<span class="nom-form">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
|
||||
<span class="badge">${escHtml(label)}</span>
|
||||
${botonesAccion}
|
||||
</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.data.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) { alert('Error: ' + json.error); return; }
|
||||
await actualizarConsentimientos(turnoId);
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Modal de firma presencial ─────────────────────────────────
|
||||
function abrirFirmaPresencial(token, consentId, nombreForm) {
|
||||
const url = BASE_WA + 'ver_formulario_enviado.php?token=' + encodeURIComponent(token);
|
||||
document.getElementById('modal-firma-titulo').textContent = 'Firmar: ' + nombreForm;
|
||||
document.getElementById('firma-iframe').src = url;
|
||||
document.getElementById('modal-firma').classList.add('open');
|
||||
|
||||
// Al cerrar, refrescar consentimientos
|
||||
window._firmaConsentId = consentId;
|
||||
}
|
||||
|
||||
function cerrarModalFirma() {
|
||||
document.getElementById('modal-firma').classList.remove('open');
|
||||
document.getElementById('firma-iframe').src = '';
|
||||
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
|
||||
}
|
||||
|
||||
// Cerrar modal al hacer clic fuera del cuadro
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('modal-firma').addEventListener('click', function(e) {
|
||||
if (e.target === this) cerrarModalFirma();
|
||||
});
|
||||
|
||||
// Escuchar mensaje del iframe (firma completada desde ver_formulario_enviado.php)
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.data && e.data.type === 'turneroFirmado') {
|
||||
cerrarModalFirma();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Acciones del turno ────────────────────────────────────────
|
||||
async function iniciarAtencion() {
|
||||
if (!turnoActivo) return;
|
||||
if (hayPendientes) return; // botón ya deshabilitado, doble seguro
|
||||
|
||||
const btn = document.getElementById('btn-iniciar');
|
||||
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_servicio' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
alert('Error: ' + json.error);
|
||||
btn.disabled = hayPendientes;
|
||||
return;
|
||||
}
|
||||
turnoActivo = json.data.turno;
|
||||
actualizarEstadoBadge('en_servicio');
|
||||
|
||||
// Mostrar "Finalizar" y "Regresar", ocultar "Iniciar"
|
||||
btn.classList.add('d-none');
|
||||
document.getElementById('btn-finalizar').classList.remove('d-none');
|
||||
document.getElementById('btn-regresar').classList.remove('d-none');
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizarAtencion() {
|
||||
if (!turnoActivo) 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 del lugar?`)) return;
|
||||
|
||||
// Regresar a en_espera_lugar
|
||||
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) { alert('Error: ' + json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + 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) { alert('Error: ' + json.error); return; }
|
||||
resetFicha();
|
||||
cargarCola();
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reset ─────────────────────────────────────────────────────
|
||||
function resetFicha() {
|
||||
clearInterval(pollingConsentId);
|
||||
turnoActivo = null;
|
||||
tieneConsent = false;
|
||||
hayPendientes = false;
|
||||
|
||||
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');
|
||||
|
||||
// Botones al estado inicial
|
||||
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');
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
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 = 'ms-auto badge ' + cls;
|
||||
el.textContent = lbl;
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str ?? '')));
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user