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