up
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GET /modules/turnero/api/get_display_recepcion.php?desk_id=X
|
||||||
|
* Devuelve el turno activo en_recepcion para un escritorio específico.
|
||||||
|
* Acceso público (pantalla TV).
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_helpers.php';
|
||||||
|
requireMethod('GET');
|
||||||
|
|
||||||
|
$deskId = (int)($_GET['desk_id'] ?? 0);
|
||||||
|
if (!$deskId) jsonError('desk_id requerido.', 400);
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Verificar que el escritorio existe y es de tipo recepcion
|
||||||
|
$stmtDesk = $pdo->prepare(
|
||||||
|
"SELECT id, nombre FROM turnero_lugares WHERE id = ? AND tipo = 'recepcion' AND activo = 1"
|
||||||
|
);
|
||||||
|
$stmtDesk->execute([$deskId]);
|
||||||
|
$desk = $stmtDesk->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$desk) jsonError('Escritorio no encontrado.', 404);
|
||||||
|
|
||||||
|
$sesionId = obtenerOCrearSesionHoy();
|
||||||
|
|
||||||
|
// Turno activo en este escritorio (el más reciente llamado_recepcion_at)
|
||||||
|
$stmtT = $pdo->prepare(
|
||||||
|
"SELECT t.id,
|
||||||
|
t.codigo,
|
||||||
|
t.numero,
|
||||||
|
t.estado,
|
||||||
|
t.paciente_nombre,
|
||||||
|
t.llamado_recepcion_at AS llamado_at,
|
||||||
|
p.codigo AS prioridad_codigo,
|
||||||
|
p.nombre AS prioridad_nombre,
|
||||||
|
p.color AS prioridad_color
|
||||||
|
FROM turnero_turnos t
|
||||||
|
JOIN turnero_prioridades p ON p.id = t.prioridad_id
|
||||||
|
WHERE t.sesion_id = ?
|
||||||
|
AND t.estado = 'en_recepcion'
|
||||||
|
AND t.recepcion_desk_id = ?
|
||||||
|
ORDER BY t.llamado_recepcion_at DESC
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmtT->execute([$sesionId, $deskId]);
|
||||||
|
$turno = $stmtT->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
|
||||||
|
// Stats básicos para el footer
|
||||||
|
$stmtStats = $pdo->prepare(
|
||||||
|
"SELECT SUM(estado = 'espera') AS en_espera,
|
||||||
|
SUM(estado IN ('en_recepcion','en_espera_lugar','en_servicio')) AS en_atencion,
|
||||||
|
SUM(estado = 'finalizado') AS finalizados
|
||||||
|
FROM turnero_turnos WHERE sesion_id = ?"
|
||||||
|
);
|
||||||
|
$stmtStats->execute([$sesionId]);
|
||||||
|
$stats = $stmtStats->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
jsonOk([
|
||||||
|
'desk_id' => (int)$desk['id'],
|
||||||
|
'desk_nombre'=> $desk['nombre'],
|
||||||
|
'turno' => $turno,
|
||||||
|
'stats' => $stats,
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
]);
|
||||||
@@ -16,6 +16,7 @@ requireTurnero();
|
|||||||
$datos = inputJson();
|
$datos = inputJson();
|
||||||
$area = $datos['area'] ?? '';
|
$area = $datos['area'] ?? '';
|
||||||
$lugarId = isset($datos['lugar_id']) ? (int) $datos['lugar_id'] : null;
|
$lugarId = isset($datos['lugar_id']) ? (int) $datos['lugar_id'] : null;
|
||||||
|
$deskId = isset($datos['desk_id']) ? (int) $datos['desk_id'] : null;
|
||||||
|
|
||||||
// ── Validación ────────────────────────────────────────────────
|
// ── Validación ────────────────────────────────────────────────
|
||||||
if (!in_array($area, ['recepcion', 'lugar'], true)) {
|
if (!in_array($area, ['recepcion', 'lugar'], true)) {
|
||||||
@@ -42,12 +43,13 @@ try {
|
|||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'UPDATE turnero_turnos
|
'UPDATE turnero_turnos
|
||||||
SET estado = "en_recepcion",
|
SET estado = "en_recepcion",
|
||||||
llamado_recepcion_at = NOW(),
|
llamado_recepcion_at = NOW(),
|
||||||
inicio_recepcion_at = NOW(),
|
inicio_recepcion_at = NOW(),
|
||||||
atendido_recepcion_por = ?
|
atendido_recepcion_por = ?,
|
||||||
|
recepcion_desk_id = COALESCE(?, recepcion_desk_id)
|
||||||
WHERE id = ? AND estado = "espera"'
|
WHERE id = ? AND estado = "espera"'
|
||||||
);
|
);
|
||||||
$stmt->execute([adminId(), $turno['id']]);
|
$stmt->execute([adminId(), $deskId, $turno['id']]);
|
||||||
|
|
||||||
if ($stmt->rowCount() === 0) {
|
if ($stmt->rowCount() === 0) {
|
||||||
// Otro proceso lo tomó primero (race condition)
|
// Otro proceso lo tomó primero (race condition)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* POST /modules/turnero/api/rellamar.php
|
||||||
|
* Re-llama un turno que ya está en_recepcion: actualiza llamado_recepcion_at = NOW()
|
||||||
|
* para que la pantalla TV detecte el cambio y vuelva a anunciar/animar.
|
||||||
|
*
|
||||||
|
* Body JSON:
|
||||||
|
* turno_id int requerido
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/_helpers.php';
|
||||||
|
requireMethod('POST');
|
||||||
|
requireTurnero();
|
||||||
|
|
||||||
|
$datos = inputJson();
|
||||||
|
$turnoId = (int)($datos['turno_id'] ?? 0);
|
||||||
|
$deskId = isset($datos['desk_id']) ? (int)$datos['desk_id'] : null;
|
||||||
|
|
||||||
|
if (!$turnoId) jsonError('turno_id es obligatorio.', 400);
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"UPDATE turnero_turnos
|
||||||
|
SET llamado_recepcion_at = NOW(),
|
||||||
|
recepcion_desk_id = COALESCE(?, recepcion_desk_id)
|
||||||
|
WHERE id = ? AND estado = 'en_recepcion'"
|
||||||
|
);
|
||||||
|
$stmt->execute([$deskId, $turnoId]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
jsonError('El turno no está en estado en_recepcion o no existe.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devolver el turno actualizado para que recepcion.php abra la ficha
|
||||||
|
$stmtT = $pdo->prepare(
|
||||||
|
"SELECT t.*,
|
||||||
|
p.codigo AS prioridad_codigo,
|
||||||
|
p.nombre AS prioridad_nombre,
|
||||||
|
p.color AS prioridad_color
|
||||||
|
FROM turnero_turnos t
|
||||||
|
JOIN turnero_prioridades p ON p.id = t.prioridad_id
|
||||||
|
WHERE t.id = ?"
|
||||||
|
);
|
||||||
|
$stmtT->execute([$turnoId]);
|
||||||
|
$turno = $stmtT->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
jsonOk(['turno' => $turno]);
|
||||||
@@ -3,6 +3,41 @@
|
|||||||
* Descriptor del módulo Turnero — Oleada 1
|
* Descriptor del módulo Turnero — Oleada 1
|
||||||
* Sistema de Turnero Inteligente con gestión de consentimientos informados.
|
* Sistema de Turnero Inteligente con gestión de consentimientos informados.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ── Links estáticos base ──────────────────────────────────────
|
||||||
|
$_trLinks = [
|
||||||
|
['name' => 'Dashboard', 'icon' => 'fas fa-tachometer-alt', 'route' => '/erp.php?m=turnero&v=dashboard'],
|
||||||
|
['name' => 'Configuración', 'icon' => 'fas fa-sliders-h', 'route' => '/erp.php?m=turnero&v=configuracion'],
|
||||||
|
['name' => 'Kiosko', 'icon' => 'fas fa-desktop', 'route' => '/erp.php?m=turnero&v=kiosko'],
|
||||||
|
['name' => 'Pantalla TV Global', 'icon' => 'fas fa-th-large', 'route' => '/erp.php?m=turnero&v=display_global'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Links dinámicos: un enlace por escritorio de Recepción ────
|
||||||
|
try {
|
||||||
|
$_trPdo = Database::getInstance()->getConnection();
|
||||||
|
$_trDesks = $_trPdo->query(
|
||||||
|
"SELECT id, nombre FROM turnero_lugares
|
||||||
|
WHERE activo = 1 AND tipo = 'recepcion'
|
||||||
|
ORDER BY sort_order ASC"
|
||||||
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($_trDesks)) {
|
||||||
|
foreach ($_trDesks as $_d) {
|
||||||
|
$_trLinks[] = [
|
||||||
|
'name' => $_d['nombre'],
|
||||||
|
'icon' => 'fas fa-concierge-bell',
|
||||||
|
'route' => '/erp.php?m=turnero&v=recepcion&desk_id=' . (int)$_d['id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sin escritorios configurados: enlace genérico
|
||||||
|
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => '/erp.php?m=turnero&v=recepcion'];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $_) {
|
||||||
|
// BD no disponible todavía (instalación inicial)
|
||||||
|
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => '/erp.php?m=turnero&v=recepcion'];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'slug' => 'turnero',
|
'slug' => 'turnero',
|
||||||
'name' => 'Turnero',
|
'name' => 'Turnero',
|
||||||
@@ -13,12 +48,5 @@ return [
|
|||||||
'sort_order' => 40,
|
'sort_order' => 40,
|
||||||
'oleada' => 1,
|
'oleada' => 1,
|
||||||
'description' => 'Sistema de turnos presenciales en recepción con prioridades, consentimientos y pantallas TV',
|
'description' => 'Sistema de turnos presenciales en recepción con prioridades, consentimientos y pantallas TV',
|
||||||
'links' => [
|
'links' => $_trLinks,
|
||||||
['name' => 'Dashboard', 'icon' => 'fas fa-tachometer-alt', 'route' => '/erp.php?m=turnero&v=dashboard'],
|
|
||||||
['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => '/erp.php?m=turnero&v=recepcion'],
|
|
||||||
['name' => 'Configuración', 'icon' => 'fas fa-sliders-h', 'route' => '/erp.php?m=turnero&v=configuracion'],
|
|
||||||
['name' => 'Kiosko', 'icon' => 'fas fa-desktop', 'route' => '/erp.php?m=turnero&v=kiosko'],
|
|
||||||
['name' => 'Pantalla TV', 'icon' => 'fas fa-tv', 'route' => '/erp.php?m=turnero&v=display'],
|
|
||||||
['name' => 'Pantalla TV Global', 'icon' => 'fas fa-th-large', 'route' => '/erp.php?m=turnero&v=display_global'],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -231,12 +231,126 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
|
|||||||
.stat-item .lbl { font-size: .6rem; color: rgba(255,255,255,.65); text-transform: uppercase; letter-spacing: 1px; }
|
.stat-item .lbl { font-size: .6rem; color: rgba(255,255,255,.65); text-transform: uppercase; letter-spacing: 1px; }
|
||||||
.stat-sep { width: 1px; height: 26px; background: rgba(255,255,255,.2); }
|
.stat-sep { width: 1px; height: 26px; background: rgba(255,255,255,.2); }
|
||||||
</style>
|
</style>
|
||||||
|
<style>
|
||||||
|
/* ══ DARK THEME OVERRIDE ═════════════════════════════════════ */
|
||||||
|
:root {
|
||||||
|
--bg: #070e1d;
|
||||||
|
--glass: rgba(255,255,255,.04);
|
||||||
|
--border:rgba(255,255,255,.08);
|
||||||
|
--muted: #4a5568;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
background: var(--bg);
|
||||||
|
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
/* Header modernize */
|
||||||
|
.pg-header::before {
|
||||||
|
content: ''; position: absolute; inset: 0; pointer-events: none;
|
||||||
|
background: linear-gradient(135deg,rgba(0,0,0,.32) 0%,rgba(0,0,0,0) 55%,rgba(255,255,255,.07) 100%);
|
||||||
|
}
|
||||||
|
.pg-header { position: relative; }
|
||||||
|
.pg-header::after {
|
||||||
|
content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 1px;
|
||||||
|
background: linear-gradient(90deg,transparent,rgba(255,255,255,.28),transparent);
|
||||||
|
}
|
||||||
|
.btn-son {
|
||||||
|
background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.2);
|
||||||
|
color: rgba(255,255,255,.82); border-radius: 8px; padding: 4px 11px;
|
||||||
|
font-size: .68rem; font-weight: 600; cursor: pointer; font-family: inherit;
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
.btn-son.on { background: rgba(34,197,94,.2); border-color: rgba(34,197,94,.35); color: #86efac; }
|
||||||
|
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 8px #22c55e; animation: pdot 2s ease-in-out infinite; }
|
||||||
|
@keyframes pdot { 0%,100%{opacity:1} 50%{opacity:.35} }
|
||||||
|
/* Override body/pg-* for dark */
|
||||||
|
.pg-body { background: var(--bg); }
|
||||||
|
.v-split { background: linear-gradient(180deg,transparent,var(--border) 12%,var(--border) 88%,transparent); width: 1px; }
|
||||||
|
.area-col { background: var(--bg); display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
.col-hdr { display: flex; align-items: center; gap: .55rem; padding: .38rem 1.1rem; font-size: .62rem; font-weight: 800; text-transform: uppercase; letter-spacing: 2.5px; color: rgba(255,255,255,.88); flex-shrink: 0; }
|
||||||
|
.col-hdr.rec { background: linear-gradient(90deg,#1e1b4b,#1e3a8a); border-bottom: 1px solid rgba(96,165,250,.18); }
|
||||||
|
.col-hdr.mue { background: linear-gradient(90deg,#052e16,#064e3b); border-bottom: 1px solid rgba(52,211,153,.18); }
|
||||||
|
.slot-grid { display: grid; grid-auto-rows: 1fr; height: 100%; overflow: hidden; }
|
||||||
|
.slot { display: flex; flex-direction: column; border-bottom: 1px solid var(--border); position: relative; overflow: hidden; }
|
||||||
|
.slot:last-child { border-bottom: none; }
|
||||||
|
.slot-hdr { display: flex; align-items: center; gap: .4rem; padding: .27rem .9rem; font-size: .59rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.8px; flex-shrink: 0; z-index: 1; position: relative; }
|
||||||
|
.slot-hdr.rec { color: #93c5fd; border-bottom: 1px solid rgba(59,130,246,.1); background: rgba(30,58,138,.22); }
|
||||||
|
.slot-hdr.mue { color: #6ee7b7; border-bottom: 1px solid rgba(16,185,129,.1); background: rgba(6,78,59,.22); }
|
||||||
|
.slot-hdr .hdot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.slot-hdr.rec .hdot { background: #60a5fa; box-shadow: 0 0 5px #60a5fa88; }
|
||||||
|
.slot-hdr.mue .hdot { background: #34d399; box-shadow: 0 0 5px #34d39988; }
|
||||||
|
.slot-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: .8rem 1rem; position: relative; overflow: hidden; }
|
||||||
|
.slot-body::after { content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity .6s; background: radial-gradient(ellipse 80% 60% at 50% 60%, var(--pc,transparent) 0%, transparent 70%); }
|
||||||
|
.slot-body.lit::after { opacity: .07; }
|
||||||
|
.slot-body::before { content: ''; position: absolute; left: 0; top: 18%; bottom: 18%; width: 3px; border-radius: 0 3px 3px 0; background: var(--pc,transparent); opacity: 0; transition: opacity .5s,background .5s; }
|
||||||
|
.slot-body.lit::before { opacity: 1; }
|
||||||
|
.lbl-dest { font-size: clamp(.5rem,.95vw,.66rem); font-weight: 700; text-transform: uppercase; letter-spacing: 3px; color: var(--muted); margin-bottom: .1rem; z-index: 1; position: relative; transition: color .4s; }
|
||||||
|
.slot-body.lit .lbl-dest { color: var(--pc); opacity: .7; }
|
||||||
|
.big-code { font-size: clamp(2.5rem,8.5vw,8rem); font-weight: 900; line-height: 1; color: var(--muted); letter-spacing: -2px; font-variant-numeric: tabular-nums; z-index: 1; position: relative; transition: color .45s; }
|
||||||
|
.slot-body.lit .big-code { color: var(--pc); }
|
||||||
|
.pac-name { font-size: clamp(.68rem,1.5vw,1.05rem); font-weight: 700; color: rgba(255,255,255,.6); margin-top: .2rem; text-align: center; min-height: 1.3em; z-index: 1; position: relative; }
|
||||||
|
.prio-badge { display: inline-flex; align-items: center; gap: .3rem; padding: .27rem .82rem; border-radius: 99px; font-size: clamp(.54rem,.92vw,.72rem); font-weight: 700; margin-top: .42rem; border: 1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.04); color: var(--muted); transition: background .4s,color .4s,border-color .4s; z-index: 1; position: relative; }
|
||||||
|
.ring-pop-el { position: absolute; top: 50%; left: 50%; width: 10px; height: 10px; border-radius: 50%; transform: translate(-50%,-50%); pointer-events: none; z-index: 0; }
|
||||||
|
.ring-pop-el.do-pop { animation: r-pop .85s ease-out forwards; }
|
||||||
|
@keyframes r-pop { 0%{transform:translate(-50%,-50%) scale(1);background:var(--pc,var(--brand));opacity:.38} 100%{transform:translate(-50%,-50%) scale(50);background:var(--pc,var(--brand));opacity:0} }
|
||||||
|
/* Cola dark */
|
||||||
|
.pg-cola { background: rgba(255,255,255,.022); border-top: 1px solid var(--border); }
|
||||||
|
.cola-lbl { color: var(--brand); }
|
||||||
|
.cola-sep { width: 1px; height: 12px; background: var(--border); flex-shrink: 0; }
|
||||||
|
.cola-chip { background: rgba(255,255,255,.04); font-size: .68rem; }
|
||||||
|
.cola-chip .cdot { width: 5px; height: 5px; border-radius: 50%; }
|
||||||
|
.cola-empty { font-size: .68rem; color: var(--muted); }
|
||||||
|
/* Footer dark */
|
||||||
|
.pg-footer { background: rgba(0,0,0,.45); border-top: 1px solid var(--border); }
|
||||||
|
.stat-item .val { font-size: clamp(.78rem,1.6vw,1rem); font-variant-numeric: tabular-nums; }
|
||||||
|
.stat-sep { background: var(--border); height: 19px; }
|
||||||
|
/* ══ ANNOUNCEMENT OVERLAY ════════════════════════════════════ */
|
||||||
|
#ann-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 9999;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
opacity: 0; pointer-events: none; transition: opacity .3s ease;
|
||||||
|
}
|
||||||
|
#ann-overlay.visible { opacity: 1; pointer-events: all; }
|
||||||
|
.ann-bg { position: absolute; inset: 0; background: rgba(4,8,20,.92); backdrop-filter: blur(18px); transition: background .4s; }
|
||||||
|
.ann-card {
|
||||||
|
position: relative; z-index: 1; width: min(600px,88vw);
|
||||||
|
background: rgba(255,255,255,.055); border: 1px solid rgba(255,255,255,.11);
|
||||||
|
border-radius: 28px; padding: 2.8rem 3.5rem 2.4rem; text-align: center;
|
||||||
|
box-shadow: 0 30px 90px rgba(0,0,0,.7), 0 0 70px var(--ac,#1565c0)44;
|
||||||
|
transform: scale(.88) translateY(14px);
|
||||||
|
transition: transform .4s cubic-bezier(.34,1.5,.64,1);
|
||||||
|
}
|
||||||
|
#ann-overlay.visible .ann-card { transform: scale(1) translateY(0); }
|
||||||
|
.ann-card::before { content: ''; position: absolute; left: 0; top: 12%; bottom: 12%; width: 4px; border-radius: 0 4px 4px 0; background: var(--ac,var(--brand)); box-shadow: 0 0 28px var(--ac,var(--brand)); }
|
||||||
|
.ann-ring { position: absolute; top: 50%; left: 50%; width: 10px; height: 10px; border-radius: 50%; transform: translate(-50%,-50%); pointer-events: none; }
|
||||||
|
.ann-ring.pop { animation: ann-pop 1.1s ease-out forwards; }
|
||||||
|
@keyframes ann-pop { 0%{transform:translate(-50%,-50%) scale(1);background:var(--ac,var(--brand));opacity:.4} 100%{transform:translate(-50%,-50%) scale(55);background:var(--ac,var(--brand));opacity:0} }
|
||||||
|
.ann-label { font-size: clamp(.68rem,1.5vw,.88rem); font-weight: 700; text-transform: uppercase; letter-spacing: 3.5px; color: rgba(255,255,255,.42); margin-bottom: .5rem; }
|
||||||
|
.ann-code { font-size: clamp(5rem,17vw,11rem); font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; color: var(--ac,var(--brand)); text-shadow: 0 0 90px var(--ac,var(--brand))55; position: relative; z-index: 1; }
|
||||||
|
.ann-pac { font-size: clamp(.85rem,2.1vw,1.25rem); font-weight: 700; color: rgba(255,255,255,.72); margin-top: .55rem; min-height: 1.5em; }
|
||||||
|
.ann-chip { display: inline-flex; align-items: center; gap: .35rem; padding: .34rem 1.1rem; border-radius: 99px; font-size: clamp(.6rem,1.05vw,.8rem); font-weight: 700; margin-top: .7rem; border: 1px solid; }
|
||||||
|
.ann-progress { width: 100%; height: 3px; background: rgba(255,255,255,.07); border-radius: 99px; margin-top: 1.7rem; overflow: hidden; }
|
||||||
|
.ann-bar { height: 100%; width: 100%; border-radius: 99px; background: var(--ac,var(--brand)); box-shadow: 0 0 12px var(--ac,var(--brand)); }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="pg-grid">
|
<!-- ══ Announcement overlay ══════════════════════════════════════ -->
|
||||||
|
<div id="ann-overlay">
|
||||||
|
<div class="ann-bg" id="ann-bg"></div>
|
||||||
|
<div class="ann-card">
|
||||||
|
<div class="ann-ring" id="ann-ring"></div>
|
||||||
|
<div class="ann-label" id="ann-label">Pase a Recepción</div>
|
||||||
|
<div class="ann-code" id="ann-code">A01</div>
|
||||||
|
<div class="ann-pac" id="ann-pac"></div>
|
||||||
|
<div class="ann-chip" id="ann-chip"></div>
|
||||||
|
<div class="ann-progress"><div class="ann-bar" id="ann-bar"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ Main layout ════════════════════════════════════════════════ -->
|
||||||
|
<div class="pg-wrap">
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="pg-header">
|
<header class="pg-header">
|
||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
<?php if ($_labLogo): ?>
|
<?php if ($_labLogo): ?>
|
||||||
@@ -248,86 +362,58 @@ $_labColor = preg_match('/^#[0-9a-fA-F]{3,8}$/', $_dispCfg['doc_color'] ?? '')
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="btn-sonido" id="btn-sonido" onclick="activarSonido()">
|
<div class="live-dot" title="En línea"></div>
|
||||||
|
<button class="btn-son" id="btn-sonido" onclick="event.stopPropagation();activarSonido()">
|
||||||
<i class="fas fa-volume-mute"></i> Sonido
|
<i class="fas fa-volume-mute"></i> Sonido
|
||||||
</button>
|
</button>
|
||||||
<div class="reloj" id="reloj">--:--:--</div>
|
<div class="reloj" id="reloj">--:--:--</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Cuerpo: Recepción | Muestras -->
|
<div class="pg-body">
|
||||||
<div class="pg-body" id="pg-body">
|
<div class="area-col">
|
||||||
|
<div class="col-hdr rec"><i class="fas fa-door-open"></i> Recepción</div>
|
||||||
<!-- Panel Recepción (dinámico: N escritorios) -->
|
<div class="slot-grid" id="rec-grid"></div>
|
||||||
<div class="area-panel" id="panel-recepcion">
|
|
||||||
<div class="area-header recepcion">
|
|
||||||
<i class="fas fa-door-open"></i> Recepción
|
|
||||||
</div>
|
|
||||||
<div class="lugares-grid" id="rec-grid">
|
|
||||||
<!-- Se genera en JS según escritorios configurados -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="v-split"></div>
|
||||||
<!-- Divisor -->
|
<div class="area-col">
|
||||||
<div class="area-divider"></div>
|
<div class="col-hdr mue"><i class="fas fa-vials"></i> Toma de Muestras</div>
|
||||||
|
<div class="slot-grid" id="lugares-grid"></div>
|
||||||
<!-- Panel Muestras (múltiples lugares) -->
|
|
||||||
<div class="area-panel">
|
|
||||||
<div class="area-header muestras">
|
|
||||||
<i class="fas fa-vials"></i> Toma de Muestras
|
|
||||||
</div>
|
|
||||||
<div class="lugares-grid" id="lugares-grid">
|
|
||||||
<!-- Se genera en JS -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cola en espera -->
|
|
||||||
<div class="pg-cola" id="pg-cola">
|
<div class="pg-cola" id="pg-cola">
|
||||||
<span class="cola-lbl"><i class="fas fa-list-ol me-1"></i>EN ESPERA</span>
|
<span class="cola-lbl"><i class="fas fa-list-ol" style="margin-right:.3rem"></i>EN ESPERA</span>
|
||||||
<span class="cola-vacia-msg" id="cola-vacia">Cola vacía</span>
|
<div class="cola-sep"></div>
|
||||||
|
<span class="cola-empty" id="cola-vacia">Cola vacía</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="pg-footer">
|
<footer class="pg-footer">
|
||||||
<div class="stat-item">
|
<div class="stat-item"><div class="val" id="st-espera">—</div><div class="lbl">En espera</div></div>
|
||||||
<div class="val" id="st-espera">—</div>
|
|
||||||
<div class="lbl">En espera</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-sep"></div>
|
<div class="stat-sep"></div>
|
||||||
<div class="stat-item">
|
<div class="stat-item"><div class="val" id="st-atencion">—</div><div class="lbl">En atención</div></div>
|
||||||
<div class="val" id="st-atencion">—</div>
|
|
||||||
<div class="lbl">En atención</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-sep"></div>
|
<div class="stat-sep"></div>
|
||||||
<div class="stat-item">
|
<div class="stat-item"><div class="val" id="st-finalizados">—</div><div class="lbl">Atendidos</div></div>
|
||||||
<div class="val" id="st-finalizados">—</div>
|
|
||||||
<div class="lbl">Atendidos hoy</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-sep"></div>
|
<div class="stat-sep"></div>
|
||||||
<div class="stat-item">
|
<div class="stat-item"><div class="val" id="st-total">—</div><div class="lbl">Total día</div></div>
|
||||||
<div class="val" id="st-total">—</div>
|
|
||||||
<div class="lbl">Total del día</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-sep"></div>
|
<div class="stat-sep"></div>
|
||||||
<div class="stat-item">
|
<div class="stat-item"><div class="val" id="st-tiempo">—</div><div class="lbl">T. promedio</div></div>
|
||||||
<div class="val" id="st-tiempo">—</div>
|
|
||||||
<div class="lbl">Tiempo prom.</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
|
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||||
|
|
||||||
// ── Reloj ──────────────────────────────────────────────────────
|
// ── Reloj ──────────────────────────────────────────────────────
|
||||||
function actualizarReloj() {
|
function tick() {
|
||||||
|
const d = new Date();
|
||||||
document.getElementById('reloj').textContent =
|
document.getElementById('reloj').textContent =
|
||||||
new Date().toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
String(d.getHours()).padStart(2,'0') + ':' +
|
||||||
|
String(d.getMinutes()).padStart(2,'0') + ':' +
|
||||||
|
String(d.getSeconds()).padStart(2,'0');
|
||||||
}
|
}
|
||||||
actualizarReloj();
|
tick(); setInterval(tick, 1000);
|
||||||
setInterval(actualizarReloj, 1000);
|
|
||||||
|
|
||||||
// ── Audio ──────────────────────────────────────────────────────
|
// ── Audio ──────────────────────────────────────────────────────
|
||||||
let audioCtx = null, sonidoActivo = false;
|
let audioCtx = null, sonidoActivo = false;
|
||||||
@@ -343,9 +429,7 @@ function activarSonido() {
|
|||||||
const btn = document.getElementById('btn-sonido');
|
const btn = document.getElementById('btn-sonido');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.innerHTML = '<i class="fas fa-volume-up"></i> Sonido ON';
|
btn.innerHTML = '<i class="fas fa-volume-up"></i> Sonido ON';
|
||||||
btn.style.background = 'rgba(34,197,94,.25)';
|
btn.classList.add('on');
|
||||||
btn.style.borderColor = 'rgba(34,197,94,.5)';
|
|
||||||
btn.onclick = null; btn.style.cursor = 'default';
|
|
||||||
setTimeout(playBeep, 100);
|
setTimeout(playBeep, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,173 +467,207 @@ function anunciarTurno(codigo, destino) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Estado previo ──────────────────────────────────────────────
|
// ── Estado previo ──────────────────────────────────────────────
|
||||||
const lastCodRecepcion = {}; // { desk_id: codigo }
|
// ── Announcement queue ────────────────────────────────────────
|
||||||
const lastCodLugar = {}; // { lugar_id: codigo }
|
const annQueue = [];
|
||||||
|
let isAnnouncing = false;
|
||||||
|
|
||||||
// ── Escape HTML ────────────────────────────────────────────────
|
function queueAnnouncement(item) {
|
||||||
function esc(str) {
|
if (annQueue.some(q => q._key === item._key)) return;
|
||||||
|
annQueue.push(item);
|
||||||
|
if (!isAnnouncing) processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function processQueue() {
|
||||||
|
if (annQueue.length === 0) { isAnnouncing = false; return; }
|
||||||
|
isAnnouncing = true;
|
||||||
|
showAnnouncement(annQueue.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAnnouncement({ codigo, destino, paciente, prio_codigo, prio_nombre, color }) {
|
||||||
|
const ov = document.getElementById('ann-overlay');
|
||||||
|
const bg = document.getElementById('ann-bg');
|
||||||
|
const ring = document.getElementById('ann-ring');
|
||||||
|
const code = document.getElementById('ann-code');
|
||||||
|
const lbl = document.getElementById('ann-label');
|
||||||
|
const pac = document.getElementById('ann-pac');
|
||||||
|
const chip = document.getElementById('ann-chip');
|
||||||
|
const bar = document.getElementById('ann-bar');
|
||||||
|
const c = color || '#1565c0';
|
||||||
|
|
||||||
|
code.textContent = codigo;
|
||||||
|
lbl.textContent = 'Pase a ' + destino;
|
||||||
|
pac.textContent = paciente || '';
|
||||||
|
chip.innerHTML = '<i class="fas fa-ticket-alt"></i> ' + esc(prio_codigo||'') + ' — ' + esc(prio_nombre||'');
|
||||||
|
|
||||||
|
ov.style.setProperty('--ac', c);
|
||||||
|
bg.style.background = 'radial-gradient(ellipse at 50% 45%, ' + c + '2a 0%, rgba(4,8,20,.94) 65%)';
|
||||||
|
chip.style.color = c;
|
||||||
|
chip.style.borderColor = c + '55';
|
||||||
|
chip.style.background = c + '18';
|
||||||
|
|
||||||
|
ov.classList.add('visible');
|
||||||
|
ring.classList.remove('pop');
|
||||||
|
void ring.offsetWidth;
|
||||||
|
ring.classList.add('pop');
|
||||||
|
|
||||||
|
bar.style.transition = 'none';
|
||||||
|
bar.style.width = '100%';
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
|
bar.style.transition = 'width 1.65s linear';
|
||||||
|
bar.style.width = '0%';
|
||||||
|
}));
|
||||||
|
|
||||||
|
anunciarTurno(codigo, destino);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ov.classList.remove('visible');
|
||||||
|
setTimeout(processQueue, 370);
|
||||||
|
}, 1950);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State ───────────────────────────────────────────────
|
||||||
|
const lastRec = {};
|
||||||
|
const lastLugar = {};
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────
|
||||||
|
function esc(s) {
|
||||||
const d = document.createElement('div');
|
const d = document.createElement('div');
|
||||||
d.appendChild(document.createTextNode(str));
|
d.appendChild(document.createTextNode(String(s ?? '')));
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Aplicar color de prioridad a un panel ─────────────────────
|
function buildSlot(id, nombre, turno, tipo) {
|
||||||
function aplicarColor(panelEl, ringEl, chipEl, color) {
|
const has = !!turno;
|
||||||
panelEl.style.setProperty('--prio-color', color);
|
const c = has ? (turno.prioridad_color || '#1565c0') : null;
|
||||||
panelEl.style.background = color + '12';
|
const pcSt = c ? '--pc:' + c + ';' : '';
|
||||||
chipEl.style.background = color + '22';
|
const cls = has ? 'slot-body lit' : 'slot-body';
|
||||||
chipEl.style.color = color;
|
const hCls = tipo === 'rec' ? 'slot-hdr rec' : 'slot-hdr mue';
|
||||||
if (ringEl) ringEl.style.setProperty('--prio-color', color);
|
const icon = tipo === 'rec' ? 'fas fa-concierge-bell' : 'fas fa-flask';
|
||||||
}
|
const cod = has ? esc(turno.codigo) : '—';
|
||||||
function limpiarColor(panelEl, ringEl, chipEl) {
|
const pac = has ? esc(turno.paciente_nombre || '') : '';
|
||||||
panelEl.style.removeProperty('--prio-color');
|
const chip = has
|
||||||
panelEl.style.background = '#fff';
|
? "<i class='fas fa-ticket-alt'></i> " + esc(turno.prioridad_codigo) + ' — ' + esc(turno.prioridad_nombre)
|
||||||
chipEl.style.background = '#f1f5f9';
|
: "<i class='fas fa-hourglass-half'></i> En espera";
|
||||||
chipEl.style.color = '#94a3b8';
|
const chipSt = has && c ? 'style="color:' + c + ';border-color:' + c + '44;background:' + c + '15"' : '';
|
||||||
if (ringEl) ringEl.style.removeProperty('--prio-color');
|
return '<div class="slot" id="slot-' + tipo + '-' + id + '">'
|
||||||
|
+ '<div class="' + hCls + '"><span class="hdot"></span><i class="' + icon + '"></i> ' + esc(nombre) + '</div>'
|
||||||
|
+ '<div class="' + cls + '" id="body-' + tipo + '-' + id + '" style="' + pcSt + '">'
|
||||||
|
+ '<div class="ring-pop-el" id="ring-' + tipo + '-' + id + '"></div>'
|
||||||
|
+ '<div class="lbl-dest">' + esc(nombre) + '</div>'
|
||||||
|
+ '<div class="big-code" id="cod-' + tipo + '-' + id + '">' + cod + '</div>'
|
||||||
|
+ '<div class="pac-name" id="pac-' + tipo + '-' + id + '">' + pac + '</div>'
|
||||||
|
+ '<div class="prio-badge" id="chip-' + tipo + '-' + id + '" ' + chipSt + '>' + chip + '</div>'
|
||||||
|
+ '</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render snapshot global ─────────────────────────────────────
|
function updateSlot(id, turno, tipo) {
|
||||||
|
const body = document.getElementById('body-' + tipo + '-' + id);
|
||||||
|
const cod = document.getElementById('cod-' + tipo + '-' + id);
|
||||||
|
const pac = document.getElementById('pac-' + tipo + '-' + id);
|
||||||
|
const chip = document.getElementById('chip-' + tipo + '-' + id);
|
||||||
|
if (!body) return false;
|
||||||
|
const c = turno ? (turno.prioridad_color || '#1565c0') : null;
|
||||||
|
if (turno) {
|
||||||
|
body.className = 'slot-body lit';
|
||||||
|
body.style.setProperty('--pc', c);
|
||||||
|
cod.textContent = turno.codigo;
|
||||||
|
pac.textContent = turno.paciente_nombre || '';
|
||||||
|
chip.innerHTML = "<i class='fas fa-ticket-alt'></i> " + esc(turno.prioridad_codigo) + ' — ' + esc(turno.prioridad_nombre);
|
||||||
|
chip.style.color = c; chip.style.borderColor = c + '44'; chip.style.background = c + '15';
|
||||||
|
} else {
|
||||||
|
body.className = 'slot-body';
|
||||||
|
body.style.removeProperty('--pc');
|
||||||
|
cod.textContent = '—';
|
||||||
|
pac.textContent = '';
|
||||||
|
chip.innerHTML = "<i class='fas fa-hourglass-half'></i> En espera";
|
||||||
|
chip.style.color = chip.style.borderColor = chip.style.background = '';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ────────────────────────────────────────────────────
|
||||||
function renderSnapshot(snap) {
|
function renderSnapshot(snap) {
|
||||||
// ── Recepción (múltiples escritorios) ───────────────────
|
|
||||||
const recList = snap.activos_recepcion || [];
|
const recList = snap.activos_recepcion || [];
|
||||||
|
const mueList = snap.lugares || [];
|
||||||
const recGrid = document.getElementById('rec-grid');
|
const recGrid = document.getElementById('rec-grid');
|
||||||
|
const mueGrid = document.getElementById('lugares-grid');
|
||||||
|
|
||||||
recGrid.innerHTML = recList.map(d => {
|
// Recepción
|
||||||
const t = d.turno;
|
const curRecIds = [...recGrid.querySelectorAll('.slot')].map(el => +el.id.replace('slot-rec-',''));
|
||||||
const color = t ? (t.prioridad_color || '#1565c0') : null;
|
const newRecIds = recList.map(d => d.desk_id);
|
||||||
const colorStyle = color ? `style="--prio-color:${color};background:${color}12"` : '';
|
if (curRecIds.length !== newRecIds.length || newRecIds.some(id => !curRecIds.includes(id))) {
|
||||||
const codStyle = color ? '' : 'style="color:#cbd5e1"';
|
recGrid.innerHTML = recList.map(d => buildSlot(d.desk_id, d.desk_nombre, d.turno, 'rec')).join('');
|
||||||
const chipStyle = color
|
} else {
|
||||||
? `style="background:${color}22;color:${color}"`
|
recList.forEach(d => updateSlot(d.desk_id, d.turno, 'rec'));
|
||||||
: 'style="background:#f1f5f9;color:#94a3b8"';
|
}
|
||||||
const chipContent = t
|
|
||||||
? `<i class='fas fa-ticket-alt'></i> ${esc(t.prioridad_codigo)} — ${esc(t.prioridad_nombre)}`
|
|
||||||
: `<i class='fas fa-hourglass-half'></i> En espera`;
|
|
||||||
const codText = t ? esc(t.codigo) : '—';
|
|
||||||
const pacText = t ? esc(t.paciente_nombre || '') : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="lugar-sub" id="sub-rec-${d.desk_id}">
|
|
||||||
<div class="area-header" style="background:#1e40af;font-size:.7rem;padding:.35rem 1rem">
|
|
||||||
<i class="fas fa-door-open"></i> ${esc(d.desk_nombre)}
|
|
||||||
</div>
|
|
||||||
<div class="area-turno" id="area-rec-${d.desk_id}" ${colorStyle}>
|
|
||||||
<div class="ring" id="ring-rec-${d.desk_id}"></div>
|
|
||||||
<div class="lbl-destino">Pase a ${esc(d.desk_nombre)}</div>
|
|
||||||
<div class="codigo-num" id="cod-rec-${d.desk_id}" ${codStyle}>${codText}</div>
|
|
||||||
<div class="pac-nombre">${pacText}</div>
|
|
||||||
<div class="prio-chip" id="chip-rec-${d.desk_id}" ${chipStyle}>${chipContent}</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Sonido/animación por desk
|
|
||||||
recList.forEach(d => {
|
recList.forEach(d => {
|
||||||
const t = d.turno;
|
const t = d.turno;
|
||||||
if (!t) { lastCodRecepcion[d.desk_id] = null; return; }
|
if (!t) { lastRec[d.desk_id] = null; return; }
|
||||||
if (t.codigo !== lastCodRecepcion[d.desk_id]) {
|
const clave = t.codigo + '|' + (t.llamado_at || '');
|
||||||
anunciarTurno(t.codigo, d.desk_nombre);
|
if (clave !== lastRec[d.desk_id]) {
|
||||||
const ring = document.getElementById(`ring-rec-${d.desk_id}`);
|
queueAnnouncement({ _key: 'rec-' + d.desk_id + '-' + clave, codigo: t.codigo, destino: d.desk_nombre, paciente: t.paciente_nombre, prio_codigo: t.prioridad_codigo, prio_nombre: t.prioridad_nombre, color: t.prioridad_color });
|
||||||
if (ring) { ring.classList.remove('animar'); void ring.offsetWidth; ring.classList.add('animar'); }
|
const ring = document.getElementById('ring-rec-' + d.desk_id);
|
||||||
lastCodRecepcion[d.desk_id] = t.codigo;
|
if (ring) { ring.classList.remove('do-pop'); void ring.offsetWidth; ring.classList.add('do-pop'); }
|
||||||
|
lastRec[d.desk_id] = clave;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Lugares (muestras) ──────────────────────────────────
|
// Muestras
|
||||||
const lugares = snap.lugares || [];
|
const curMueIds = [...mueGrid.querySelectorAll('.slot')].map(el => +el.id.replace('slot-mue-',''));
|
||||||
const grid = document.getElementById('lugares-grid');
|
const newMueIds = mueList.map(l => l.lugar_id);
|
||||||
|
if (curMueIds.length !== newMueIds.length || newMueIds.some(id => !curMueIds.includes(id))) {
|
||||||
// Construir HTML para todos los lugares
|
mueGrid.innerHTML = mueList.map(l => buildSlot(l.lugar_id, l.lugar_nombre, l.turno, 'mue')).join('');
|
||||||
grid.innerHTML = lugares.map(l => {
|
|
||||||
const t = l.turno;
|
|
||||||
const color = t ? (t.prioridad_color || '#15803d') : null;
|
|
||||||
|
|
||||||
const colorStyle = color ? `style="--prio-color:${color};background:${color}12"` : '';
|
|
||||||
const codStyle = color ? '' : 'style="color:#cbd5e1"';
|
|
||||||
const chipStyle = color
|
|
||||||
? `style="background:${color}22;color:${color}"`
|
|
||||||
: 'style="background:#f1f5f9;color:#94a3b8"';
|
|
||||||
const chipContent = t
|
|
||||||
? `<i class='fas fa-ticket-alt'></i> ${esc(t.prioridad_codigo)} — ${esc(t.prioridad_nombre)}`
|
|
||||||
: `<i class='fas fa-hourglass-half'></i> En espera`;
|
|
||||||
const codText = t ? esc(t.codigo) : '—';
|
|
||||||
const pacText = t ? esc(t.paciente_nombre || '') : '';
|
|
||||||
const ringId = `ring-lugar-${l.lugar_id}`;
|
|
||||||
const panelId = `area-lugar-${l.lugar_id}`;
|
|
||||||
const codId = `cod-lugar-${l.lugar_id}`;
|
|
||||||
const chipId = `chip-lugar-${l.lugar_id}`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="lugar-sub" id="sub-lugar-${l.lugar_id}">
|
|
||||||
<div class="area-header" style="background:#166534;font-size:.7rem;padding:.35rem 1rem">
|
|
||||||
<i class="fas fa-flask"></i> ${esc(l.lugar_nombre)}
|
|
||||||
</div>
|
|
||||||
<div class="area-turno" id="${panelId}" ${colorStyle}>
|
|
||||||
<div class="ring" id="${ringId}"></div>
|
|
||||||
<div class="lbl-destino">Pase a ${esc(l.lugar_nombre)}</div>
|
|
||||||
<div class="codigo-num" id="${codId}" ${codStyle}>${codText}</div>
|
|
||||||
<div class="pac-nombre">${pacText}</div>
|
|
||||||
<div class="prio-chip" id="${chipId}" ${chipStyle}>${chipContent}</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Animaciones/sonido para lugares que cambiaron
|
|
||||||
lugares.forEach(l => {
|
|
||||||
const t = l.turno;
|
|
||||||
if (!t) { lastCodLugar[l.lugar_id] = null; return; }
|
|
||||||
if (t.codigo !== lastCodLugar[l.lugar_id]) {
|
|
||||||
anunciarTurno(t.codigo, l.lugar_nombre);
|
|
||||||
const ring = document.getElementById(`ring-lugar-${l.lugar_id}`);
|
|
||||||
if (ring) {
|
|
||||||
ring.classList.remove('animar');
|
|
||||||
void ring.offsetWidth;
|
|
||||||
ring.classList.add('animar');
|
|
||||||
}
|
|
||||||
lastCodLugar[l.lugar_id] = t.codigo;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Cola en espera ───────────────────────────────────────
|
|
||||||
const cola = snap.cola || [];
|
|
||||||
const pgCola = document.getElementById('pg-cola');
|
|
||||||
const vaciaMg = document.getElementById('cola-vacia');
|
|
||||||
|
|
||||||
// Limpiar chips anteriores (conservar el label y el mensaje)
|
|
||||||
pgCola.querySelectorAll('.cola-chip').forEach(c => c.remove());
|
|
||||||
|
|
||||||
if (cola.length === 0) {
|
|
||||||
if (vaciaMg) vaciaMg.style.display = '';
|
|
||||||
} else {
|
} else {
|
||||||
if (vaciaMg) vaciaMg.style.display = 'none';
|
mueList.forEach(l => updateSlot(l.lugar_id, l.turno, 'mue'));
|
||||||
|
}
|
||||||
|
mueList.forEach(l => {
|
||||||
|
const t = l.turno;
|
||||||
|
if (!t) { lastLugar[l.lugar_id] = null; return; }
|
||||||
|
const clave = t.codigo + '|' + (t.llamado_at || '');
|
||||||
|
if (clave !== lastLugar[l.lugar_id]) {
|
||||||
|
queueAnnouncement({ _key: 'mue-' + l.lugar_id + '-' + clave, codigo: t.codigo, destino: l.lugar_nombre, paciente: t.paciente_nombre, prio_codigo: t.prioridad_codigo, prio_nombre: t.prioridad_nombre, color: t.prioridad_color });
|
||||||
|
const ring = document.getElementById('ring-mue-' + l.lugar_id);
|
||||||
|
if (ring) { ring.classList.remove('do-pop'); void ring.offsetWidth; ring.classList.add('do-pop'); }
|
||||||
|
lastLugar[l.lugar_id] = clave;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cola
|
||||||
|
const cola = snap.cola || [];
|
||||||
|
const pgCola = document.getElementById('pg-cola');
|
||||||
|
pgCola.querySelectorAll('.cola-chip').forEach(c => c.remove());
|
||||||
|
const vaciaMg = document.getElementById('cola-vacia');
|
||||||
|
if (cola.length === 0) {
|
||||||
|
vaciaMg.style.display = '';
|
||||||
|
} else {
|
||||||
|
vaciaMg.style.display = 'none';
|
||||||
cola.forEach(t => {
|
cola.forEach(t => {
|
||||||
const chip = document.createElement('span');
|
const chip = document.createElement('span');
|
||||||
chip.className = 'cola-chip';
|
const c = t.prioridad_color || '#1565c0';
|
||||||
chip.style.color = t.prioridad_color || '#1565c0';
|
chip.className = 'cola-chip';
|
||||||
chip.style.borderColor = t.prioridad_color || '#1565c0';
|
chip.style.color = c; chip.style.borderColor = c + '55'; chip.style.background = c + '12';
|
||||||
chip.innerHTML = `<span style="width:8px;height:8px;border-radius:50%;background:${t.prioridad_color || '#1565c0'};display:inline-block"></span> ${esc(t.codigo)}`;
|
chip.innerHTML = '<span class="cdot" style="background:' + c + ';box-shadow:0 0 5px ' + c + '88"></span>' + esc(t.codigo);
|
||||||
pgCola.appendChild(chip);
|
pgCola.appendChild(chip);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stats ────────────────────────────────────────────────
|
// Stats
|
||||||
const s = snap.stats || {};
|
const s = snap.stats || {};
|
||||||
document.getElementById('st-espera').textContent = s.en_espera || 0;
|
document.getElementById('st-espera').textContent = s.en_espera || 0;
|
||||||
document.getElementById('st-atencion').textContent = s.en_atencion || 0;
|
document.getElementById('st-atencion').textContent = s.en_atencion || 0;
|
||||||
document.getElementById('st-finalizados').textContent = s.finalizados || 0;
|
document.getElementById('st-finalizados').textContent = s.finalizados || 0;
|
||||||
document.getElementById('st-total').textContent = s.total || 0;
|
document.getElementById('st-total').textContent = s.total || 0;
|
||||||
document.getElementById('st-tiempo').textContent = s.tiempo_promedio_atencion
|
document.getElementById('st-tiempo').textContent = s.tiempo_promedio_atencion
|
||||||
? s.tiempo_promedio_atencion.substring(0, 5) : '—';
|
? s.tiempo_promedio_atencion.substring(0, 5) : '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Polling cada 4 s ──────────────────────────────────────────
|
// ── Polling ───────────────────────────────────────────────────
|
||||||
async function cargarSnapshot() {
|
async function cargarSnapshot() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(BASE_API + 'get_display_global.php', { cache: 'no-store' });
|
const res = await fetch(BASE_API + 'get_display_global.php', { cache: 'no-store' });
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.ok) renderSnapshot(json);
|
if (json.ok) renderSnapshot(json);
|
||||||
} catch (_) {}
|
} catch(_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
cargarSnapshot();
|
cargarSnapshot();
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* display_recepcion.php — Pantalla TV por escritorio de Recepción
|
||||||
|
* Acceso: erp.php?m=turnero&v=display_recepcion&desk_id=X
|
||||||
|
* Sin autenticación (pantalla pública de TV).
|
||||||
|
*/
|
||||||
|
|
||||||
|
$_deskId = (int)($_GET['desk_id'] ?? 0);
|
||||||
|
$_deskNom = 'Recepción';
|
||||||
|
$_labNom = 'Sistema de Turnos';
|
||||||
|
$_labLogo = '';
|
||||||
|
$_labColor = '#1565c0';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$_pdo = Database::getInstance()->getConnection();
|
||||||
|
|
||||||
|
// Nombre del escritorio
|
||||||
|
if ($_deskId) {
|
||||||
|
$_row = $_pdo->prepare("SELECT nombre FROM turnero_lugares WHERE id = ? AND tipo='recepcion' AND activo=1");
|
||||||
|
$_row->execute([$_deskId]);
|
||||||
|
$_found = $_row->fetchColumn();
|
||||||
|
if ($_found) $_deskNom = $_found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config visual del lab
|
||||||
|
$_cfg = $_pdo->query(
|
||||||
|
"SELECT clave, valor FROM lab_config WHERE clave IN ('empresa_nombre','doc_logo_base64','doc_color')"
|
||||||
|
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
$_labNom = htmlspecialchars($_cfg['empresa_nombre'] ?? 'Sistema de Turnos');
|
||||||
|
$_labLogo = $_cfg['doc_logo_base64'] ?? '';
|
||||||
|
if (preg_match('/^#[0-9a-fA-F]{3,8}$/', $_cfg['doc_color'] ?? '')) {
|
||||||
|
$_labColor = $_cfg['doc_color'];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $_) {}
|
||||||
|
|
||||||
|
$_deskNomSafe = htmlspecialchars($_deskNom);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= $_deskNomSafe ?> — Pantalla TV</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0; padding: 0; height: 100%; width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
background: #f0f4f8; color: #1e293b;
|
||||||
|
}
|
||||||
|
:root {
|
||||||
|
--brand: <?= $_labColor ?>;
|
||||||
|
--prio-color: <?= $_labColor ?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.pg-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.pg-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: .7rem 2rem;
|
||||||
|
background: var(--brand); gap: 1rem; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.header-brand { display: flex; align-items: center; gap: .8rem; flex: 1; min-width: 0; }
|
||||||
|
.header-brand img.logo {
|
||||||
|
height: 44px; max-width: 120px; object-fit: contain;
|
||||||
|
background: rgba(255,255,255,.12); border-radius: 8px; padding: 4px 6px;
|
||||||
|
}
|
||||||
|
.lab-nombre {
|
||||||
|
font-size: clamp(.9rem, 2vw, 1.3rem); font-weight: 800; color: #fff;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.lab-sub {
|
||||||
|
font-size: .68rem; color: rgba(255,255,255,.7); font-weight: 500;
|
||||||
|
text-transform: uppercase; letter-spacing: 1.5px; display: block; margin-top: 1px;
|
||||||
|
}
|
||||||
|
.reloj {
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.6rem); font-weight: 800; color: #fff;
|
||||||
|
font-variant-numeric: tabular-nums; letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.btn-sonido {
|
||||||
|
background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.3);
|
||||||
|
color: #fff; border-radius: 8px; padding: 5px 12px; font-size: .78rem; cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cuerpo central */
|
||||||
|
.pg-body {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
padding: 2rem; overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjeta principal del turno */
|
||||||
|
.turno-card {
|
||||||
|
width: min(600px, 88vw);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,.12);
|
||||||
|
border-top: 6px solid var(--prio-color);
|
||||||
|
padding: 3rem 3.5rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
transition: border-color .4s, background .4s;
|
||||||
|
}
|
||||||
|
.turno-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute; inset: 0; border-radius: 24px;
|
||||||
|
border: 3px solid var(--prio-color);
|
||||||
|
opacity: .3; pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Etiqueta destino */
|
||||||
|
.lbl-destino {
|
||||||
|
font-size: clamp(.85rem, 2vw, 1.1rem);
|
||||||
|
font-weight: 600; color: #64748b;
|
||||||
|
text-transform: uppercase; letter-spacing: 2px;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ring de animación */
|
||||||
|
.ring {
|
||||||
|
position: absolute; top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 0; height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none; z-index: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.ring.animar {
|
||||||
|
animation: ringPulse 1.4s ease-out forwards;
|
||||||
|
background: var(--prio-color);
|
||||||
|
}
|
||||||
|
@keyframes ringPulse {
|
||||||
|
0% { width: 0; height: 0; opacity: .5; }
|
||||||
|
100% { width: 120%; height: 120%; opacity: 0; margin: -60% 0 0 -60%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Código grande */
|
||||||
|
.codigo-num {
|
||||||
|
font-size: clamp(5rem, 18vw, 12rem);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--prio-color);
|
||||||
|
letter-spacing: -4px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
transition: color .4s;
|
||||||
|
}
|
||||||
|
.codigo-num.vacio { color: #cbd5e1; }
|
||||||
|
|
||||||
|
/* Nombre paciente */
|
||||||
|
.pac-nombre {
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.6rem);
|
||||||
|
font-weight: 600; color: #334155;
|
||||||
|
margin-top: .5rem; min-height: 1.8em;
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chip prioridad */
|
||||||
|
.prio-chip {
|
||||||
|
display: inline-flex; align-items: center; gap: .4rem;
|
||||||
|
background: #f1f5f9; color: #94a3b8;
|
||||||
|
border-radius: 50px; padding: .4rem 1rem;
|
||||||
|
font-size: .85rem; font-weight: 600;
|
||||||
|
margin-top: 1.2rem; transition: background .4s, color .4s;
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.pg-footer {
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 2.5rem;
|
||||||
|
padding: .6rem 2rem;
|
||||||
|
background: #1e293b; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.stat-item { text-align: center; }
|
||||||
|
.stat-item .val {
|
||||||
|
font-size: clamp(.9rem, 2vw, 1.3rem); font-weight: 800; color: #f1f5f9;
|
||||||
|
}
|
||||||
|
.stat-item .lbl { font-size: .65rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
.stat-sep { width: 1px; height: 28px; background: rgba(255,255,255,.12); }
|
||||||
|
|
||||||
|
/* Sin escritorio */
|
||||||
|
.no-desk {
|
||||||
|
text-align: center; color: #64748b; padding: 3rem;
|
||||||
|
}
|
||||||
|
.no-desk i { font-size: 3rem; color: #cbd5e1; margin-bottom: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body onclick="activarSonido()">
|
||||||
|
<div class="pg-wrap">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="pg-header">
|
||||||
|
<div class="header-brand">
|
||||||
|
<?php if ($_labLogo): ?>
|
||||||
|
<img class="logo" src="<?= $_labLogo ?>" alt="Logo">
|
||||||
|
<?php endif; ?>
|
||||||
|
<div>
|
||||||
|
<div class="lab-nombre"><?= $_labNom ?></div>
|
||||||
|
<span class="lab-sub"><?= $_deskNomSafe ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:.75rem">
|
||||||
|
<button class="btn-sonido" id="btn-sonido">
|
||||||
|
<i class="fas fa-volume-mute"></i> Sonido
|
||||||
|
</button>
|
||||||
|
<div class="reloj" id="reloj">--:--:--</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Cuerpo -->
|
||||||
|
<main class="pg-body">
|
||||||
|
<?php if (!$_deskId): ?>
|
||||||
|
<div class="no-desk">
|
||||||
|
<i class="fas fa-exclamation-circle d-block"></i>
|
||||||
|
<p class="fw-semibold">No se especificó un escritorio de recepción.</p>
|
||||||
|
<small>Usa <code>?desk_id=X</code> en la URL.</small>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="turno-card" id="turno-card">
|
||||||
|
<div class="ring" id="ring-turno"></div>
|
||||||
|
<div class="lbl-destino">Pase a <?= $_deskNomSafe ?></div>
|
||||||
|
<div class="codigo-num vacio" id="cod-turno">—</div>
|
||||||
|
<div class="pac-nombre" id="pac-turno"></div>
|
||||||
|
<div class="prio-chip" id="chip-turno">
|
||||||
|
<i class="fas fa-hourglass-half"></i> En espera
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer stats -->
|
||||||
|
<footer class="pg-footer">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="val" id="st-espera">—</div>
|
||||||
|
<div class="lbl">En espera</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-sep"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="val" id="st-atencion">—</div>
|
||||||
|
<div class="lbl">En atención</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-sep"></div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="val" id="st-finalizados">—</div>
|
||||||
|
<div class="lbl">Finalizados</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const BASE_API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||||
|
const DESK_ID = <?= $_deskId ?: 'null' ?>;
|
||||||
|
const DESK_NOM = <?= json_encode($_deskNom) ?>;
|
||||||
|
let sonidoActivo = false;
|
||||||
|
let lastClave = null; // codigo|llamado_at
|
||||||
|
|
||||||
|
// ── Reloj ─────────────────────────────────────────────────────
|
||||||
|
function tick() {
|
||||||
|
const d = new Date();
|
||||||
|
document.getElementById('reloj').textContent =
|
||||||
|
String(d.getHours()).padStart(2,'0') + ':' +
|
||||||
|
String(d.getMinutes()).padStart(2,'0') + ':' +
|
||||||
|
String(d.getSeconds()).padStart(2,'0');
|
||||||
|
}
|
||||||
|
tick(); setInterval(tick, 1000);
|
||||||
|
|
||||||
|
// ── Activar TTS ───────────────────────────────────────────────
|
||||||
|
function activarSonido() {
|
||||||
|
if (sonidoActivo) return;
|
||||||
|
sonidoActivo = true;
|
||||||
|
document.getElementById('btn-sonido').innerHTML = '<i class="fas fa-volume-up"></i> Sonido';
|
||||||
|
document.getElementById('btn-sonido').style.background = 'rgba(255,255,255,.35)';
|
||||||
|
}
|
||||||
|
document.getElementById('btn-sonido').addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
activarSonido();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── TTS ───────────────────────────────────────────────────────
|
||||||
|
function anunciarTurno(codigo) {
|
||||||
|
if (!sonidoActivo || !('speechSynthesis' in window)) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
const letras = codigo.split('').join(' ');
|
||||||
|
const utt = new SpeechSynthesisUtterance(`Turno ${letras}, pase a ${DESK_NOM}`);
|
||||||
|
utt.lang = 'es-CO'; utt.rate = 0.85; utt.pitch = 1.05; utt.volume = 1;
|
||||||
|
window.speechSynthesis.speak(utt);
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Escape HTML ───────────────────────────────────────────────
|
||||||
|
function esc(s) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.appendChild(document.createTextNode(s));
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ────────────────────────────────────────────────────
|
||||||
|
function render(snap) {
|
||||||
|
const t = snap.turno;
|
||||||
|
const card = document.getElementById('turno-card');
|
||||||
|
const ring = document.getElementById('ring-turno');
|
||||||
|
const cod = document.getElementById('cod-turno');
|
||||||
|
const pac = document.getElementById('pac-turno');
|
||||||
|
const chip = document.getElementById('chip-turno');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
const color = t.prioridad_color || '#1565c0';
|
||||||
|
const clave = t.codigo + '|' + (t.llamado_at || '');
|
||||||
|
|
||||||
|
// Actualizar colores
|
||||||
|
card.style.setProperty('--prio-color', color);
|
||||||
|
card.style.background = '#fff';
|
||||||
|
card.style.borderTopColor = color;
|
||||||
|
chip.style.background = color + '22';
|
||||||
|
chip.style.color = color;
|
||||||
|
|
||||||
|
cod.textContent = t.codigo;
|
||||||
|
cod.className = 'codigo-num';
|
||||||
|
cod.style.color = '';
|
||||||
|
pac.textContent = t.paciente_nombre || '';
|
||||||
|
chip.innerHTML = `<i class='fas fa-ticket-alt'></i> ${esc(t.prioridad_codigo)} — ${esc(t.prioridad_nombre)}`;
|
||||||
|
|
||||||
|
if (clave !== lastClave) {
|
||||||
|
anunciarTurno(t.codigo);
|
||||||
|
ring.classList.remove('animar');
|
||||||
|
void ring.offsetWidth;
|
||||||
|
ring.classList.add('animar');
|
||||||
|
lastClave = clave;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
card.style.removeProperty('--prio-color');
|
||||||
|
card.style.background = '#fff';
|
||||||
|
card.style.borderTopColor = '#e2e8f0';
|
||||||
|
chip.style.background = '#f1f5f9';
|
||||||
|
chip.style.color = '#94a3b8';
|
||||||
|
|
||||||
|
cod.textContent = '—';
|
||||||
|
cod.className = 'codigo-num vacio';
|
||||||
|
pac.textContent = '';
|
||||||
|
chip.innerHTML = '<i class="fas fa-hourglass-half"></i> En espera';
|
||||||
|
lastClave = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats footer
|
||||||
|
const s = snap.stats || {};
|
||||||
|
const el = id => document.getElementById(id);
|
||||||
|
el('st-espera').textContent = s.en_espera || 0;
|
||||||
|
el('st-atencion').textContent = s.en_atencion || 0;
|
||||||
|
el('st-finalizados').textContent= s.finalizados || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Polling ───────────────────────────────────────────────────
|
||||||
|
async function cargar() {
|
||||||
|
if (!DESK_ID) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(BASE_API + 'get_display_recepcion.php?desk_id=' + DESK_ID, { cache: 'no-store' });
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.ok) render(json);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
cargar();
|
||||||
|
setInterval(cargar, 4000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -27,11 +27,26 @@ try {
|
|||||||
$cat = $ex['categoria'] ?: 'General';
|
$cat = $ex['categoria'] ?: 'General';
|
||||||
$examenesAgrupados[$cat][] = $ex;
|
$examenesAgrupados[$cat][] = $ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escritorio de recepción (opcional, vía ?desk_id=X)
|
||||||
|
$deskId = (int)($_GET['desk_id'] ?? 0);
|
||||||
|
$desk = null;
|
||||||
|
if ($deskId) {
|
||||||
|
$stmtDesk = $pdo->prepare(
|
||||||
|
"SELECT id, nombre FROM turnero_lugares WHERE id = ? AND tipo = 'recepcion' AND activo = 1"
|
||||||
|
);
|
||||||
|
$stmtDesk->execute([$deskId]);
|
||||||
|
$desk = $stmtDesk->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
if (!$desk) { $deskId = 0; } // ID inválido → sin desk
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$lugares = [];
|
$lugares = [];
|
||||||
$examenesAgrupados = [];
|
$examenesAgrupados = [];
|
||||||
|
$deskId = 0;
|
||||||
|
$desk = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$deskNombre = $desk['nombre'] ?? null; // null = vista genérica
|
||||||
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
|
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -39,7 +54,7 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Recepción — Turnero</title>
|
<title><?= htmlspecialchars($deskNombre ?? 'Recepción') ?> — Turnero</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
|
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
|
||||||
@@ -169,17 +184,19 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
|||||||
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
|
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
|
||||||
style="position:sticky;top:0;z-index:100">
|
style="position:sticky;top:0;z-index:100">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<i class="fas fa-ticket-alt text-primary"></i>
|
<i class="fas fa-concierge-bell text-primary"></i>
|
||||||
<strong>Recepción</strong>
|
<strong><?= htmlspecialchars($deskNombre ?? 'Recepción') ?></strong>
|
||||||
<span id="badge-turno-activo" class="badge-turno-activo d-none">
|
<span id="badge-turno-activo" class="badge-turno-activo d-none">
|
||||||
Turno: <span id="badge-codigo">—</span>
|
Turno: <span id="badge-codigo">—</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
||||||
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=lugar" class="btn btn-outline-secondary btn-sm" target="_blank">
|
<?php if ($deskId): ?>
|
||||||
<i class="fas fa-flask me-1"></i>Ver Lugar
|
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=display_recepcion&desk_id=<?= $deskId ?>" class="btn btn-outline-info btn-sm" target="_blank">
|
||||||
|
<i class="fas fa-tv me-1"></i>Ver Pantalla
|
||||||
</a>
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
||||||
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
||||||
</button>
|
</button>
|
||||||
@@ -364,8 +381,9 @@ let solicitudActiva = null;
|
|||||||
let consentimientos = [];
|
let consentimientos = [];
|
||||||
let pollingColaId = null;
|
let pollingColaId = null;
|
||||||
|
|
||||||
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
||||||
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
|
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
|
||||||
|
const DESK_ID = <?= $deskId ?: 'null' ?>;
|
||||||
|
|
||||||
// ── Arranque ──────────────────────────────────────────────────
|
// ── Arranque ──────────────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -421,7 +439,7 @@ async function llamarSiguiente() {
|
|||||||
const res = await fetch(API + 'llamar_turno.php', {
|
const res = await fetch(API + 'llamar_turno.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ area: 'recepcion' }),
|
body: JSON.stringify({ area: 'recepcion', desk_id: DESK_ID }),
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!json.ok) { mostrarError(json.error); return; }
|
if (!json.ok) { mostrarError(json.error); return; }
|
||||||
@@ -454,8 +472,18 @@ async function seleccionarTurno(turnoId) {
|
|||||||
|
|
||||||
// ── Llamar turno específico (cambia estado → en_recepcion) ─────────
|
// ── Llamar turno específico (cambia estado → en_recepcion) ─────────
|
||||||
async function llamarTurnoEspecifico(turnoId, estadoActual) {
|
async function llamarTurnoEspecifico(turnoId, estadoActual) {
|
||||||
// Si ya está en recepción, solo abrir ficha sin intentar re-transicionar
|
// Si ya está en recepción: re-llamar (actualiza llamado_at para que la pantalla TV reactive)
|
||||||
if (estadoActual === 'en_recepcion') {
|
if (estadoActual === 'en_recepcion') {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API + 'rellamar.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ turno_id: turnoId, desk_id: DESK_ID }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.ok && json.turno) { abrirFicha(json.turno); return; }
|
||||||
|
} catch (_) {}
|
||||||
|
// Fallback: solo abrir ficha
|
||||||
seleccionarTurno(turnoId);
|
seleccionarTurno(turnoId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user