1248 lines
57 KiB
PHP
1248 lines
57 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();
|
|
|
|
// Para destino solo nos interesa recepción y toma de muestras
|
|
$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);
|
|
$lugarMuestras = $pdo->query(
|
|
"SELECT id, nombre FROM turnero_lugares WHERE tipo='muestras' AND activo=1 ORDER BY sort_order LIMIT 1"
|
|
)->fetch(PDO::FETCH_ASSOC);
|
|
|
|
$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 (opcional, vía ?desk_id=X)
|
|
$deskId = (int)($_GET['desk_id'] ?? 0);
|
|
$desk = null;
|
|
if ($deskId) {
|
|
$stmtDesk = $pdo->prepare(
|
|
"SELECT id, nombre FROM turnero_lugares WHERE id = ? AND tipo = 'recepcion' AND activo = 1"
|
|
);
|
|
$stmtDesk->execute([$deskId]);
|
|
$desk = $stmtDesk->fetch(PDO::FETCH_ASSOC) ?: null;
|
|
if (!$desk) { $deskId = 0; } // ID inválido → sin desk
|
|
}
|
|
} 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; }
|
|
.rec-cola { max-height: 260px; }
|
|
}
|
|
|
|
/* ── 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; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<?php
|
|
$SIDEBAR_TITLE = 'Turnero';
|
|
$SIDEBAR_ICON = 'fas fa-ticket-alt';
|
|
require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
|
?>
|
|
|
|
<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">
|
|
<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">
|
|
<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 ── -->
|
|
<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">Nombre del kiosko:</small>
|
|
<div class="fw-semibold" id="lbl-nombre-kiosko"></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>
|
|
<button class="btn btn-link btn-sm text-secondary p-0" onclick="desvincularPaciente()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</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 if ($lugarRecepcion): ?>
|
|
<option value="<?= (int)$lugarRecepcion['id'] ?>">Recepción</option>
|
|
<?php endif; ?>
|
|
<?php if ($lugarMuestras): ?>
|
|
<option value="<?= (int)$lugarMuestras['id'] ?>">Toma de muestras</option>
|
|
<?php endif; ?>
|
|
</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 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">
|
|
<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>
|
|
</select>
|
|
</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>
|
|
|
|
<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 DESK_ID = <?= $deskId ?: 'null' ?>;
|
|
|
|
// ── 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 }),
|
|
});
|
|
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 ───────────────────────────────────────────────
|
|
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');
|
|
|
|
// 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
|
|
if (turno.paciente_nombre) {
|
|
document.getElementById('bloque-nombre-kiosko').classList.remove('d-none');
|
|
document.getElementById('lbl-nombre-kiosko').textContent = turno.paciente_nombre;
|
|
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);
|
|
}
|
|
|
|
// ── 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="abrirNuevoPaciente('${escHtml(q)}');return false">Crear nuevo</a></div>`;
|
|
return;
|
|
}
|
|
lista.innerHTML = datos.map(p => `
|
|
<div class="pac-resultado" onclick="seleccionarPaciente(${JSON.stringify(p).replace(/"/g,'"')})">
|
|
<div class="pac-nombre">${escHtml((p.full_name||p.nombre||'') + ' ' + (p.apellido||''))}</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;
|
|
document.getElementById('bloque-pac-no-vinculado').classList.add('d-none');
|
|
document.getElementById('bloque-pac-seleccionado').classList.remove('d-none');
|
|
document.getElementById('lbl-pac-nombre').textContent =
|
|
(pac.full_name || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
|
|
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 = '';
|
|
}
|
|
|
|
function desvincularPaciente() {
|
|
pacienteActivo = null;
|
|
document.getElementById('bloque-pac-no-vinculado').classList.remove('d-none');
|
|
document.getElementById('bloque-pac-seleccionado').classList.add('d-none');
|
|
document.getElementById('lista-pacientes-res').innerHTML = '';
|
|
document.getElementById('inp-buscar-pac').value = '';
|
|
}
|
|
|
|
function abrirNuevoPaciente(nombre) {
|
|
window.open('<?= BASE_URL ?>lab_pacientes.php?nuevo=1&nombre=' + encodeURIComponent(nombre), '_blank');
|
|
}
|
|
|
|
// ── 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; }
|
|
|
|
// ✅ VALIDACIÓN: verificar que los consentimientos del lugar estén firmados
|
|
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 (!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,
|
|
total_cobrado: parseFloat(document.getElementById('inp-total').value) || null,
|
|
metodo_pago: document.getElementById('sel-pago').value || null,
|
|
observaciones: document.getElementById('inp-obs').value.trim() || 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 enviar/reenviar WhatsApp
|
|
const btnWa = ya
|
|
? (c.token ? `<button class="btn btn-outline-secondary" onclick="verFirmado('${token}')" title="Ver formulario firmado">
|
|
<i class="fas fa-eye"></i> Ver
|
|
</button>` : '')
|
|
: `<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 visible si el paciente ya firmó)
|
|
let profRow = '';
|
|
if (c.estado === 'firmado') {
|
|
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.getOrCreate(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.getInstance(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 }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok && json.data?.url) {
|
|
window.open(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. Llamando siguiente…', 'success', 2000);
|
|
|
|
// Llamar siguiente automáticamente después de 1.5 s
|
|
setTimeout(() => {
|
|
cargarCola();
|
|
resetPlaceholder();
|
|
// Auto-llamar siguiente turno si hay cola
|
|
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
|
document.getElementById('ficha-placeholder').innerHTML = `
|
|
<i class="fas fa-hourglass-half fa-3x mb-3" style="color:#3b82f6"></i>
|
|
<p class="fw-semibold mb-0">Llamando siguiente turno…</p>`;
|
|
llamarSiguiente();
|
|
}, 1500);
|
|
|
|
} 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();
|
|
}
|
|
|
|
// ── 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();
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────
|
|
function resetCheckboxes() {
|
|
document.querySelectorAll('.exam-chk').forEach(c => c.checked = false);
|
|
}
|
|
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;
|
|
}
|
|
|
|
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>`;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// ── 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>
|
|
</body>
|
|
</html>
|