turnero: panel especialidades (ginecología/pediatría) en toma de muestras

- Sección en la parte superior de lugar.php muestra pacientes esperando
  en Ginecología y Pediatría (inicio_lugar_at IS NOT NULL, fin_lugar_at IS NULL)
- Chips de acceso rápido (botones) al tope de la cola para navegar directo
- Contadores de espera actualizados cada 30s
- Clicking en un paciente o "Ir →" navega a esa vista completa para atenderlo
- Solo aparece si existen esos lugares en DB y hay pacientes esperando
- Nuevo: get_especialidades.php para consultar pacientes de lugares especiales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-27 22:31:11 -05:00
co-authored by Claude Sonnet 4.6
parent b1d1c0896f
commit e634eb8932
2 changed files with 162 additions and 0 deletions
@@ -0,0 +1,46 @@
<?php
/**
* GET get_especialidades.php?ids[]=20&ids[]=21
* Devuelve pacientes esperando en lugares de especialidad (ginecología, pediatría).
* "Esperando" = inicio_lugar_at IS NOT NULL AND fin_lugar_at IS NULL
*/
require_once __DIR__ . '/_helpers.php';
requireTurnero();
requireMethod('GET');
$pdo = db();
$ids = array_filter(array_map('intval', (array)($_GET['ids'] ?? [])));
if (!$ids) jsonOk(['grupos' => []]);
$sesionId = (int)($pdo->query(
"SELECT id FROM turnero_sesiones ORDER BY id DESC LIMIT 1"
)->fetchColumn() ?: 0);
if (!$sesionId) jsonOk(['grupos' => []]);
$in = implode(',', $ids);
$lugares = $pdo->query(
"SELECT id, nombre FROM turnero_lugares WHERE id IN ($in) AND activo=1 ORDER BY sort_order"
)->fetchAll(PDO::FETCH_ASSOC);
$grupos = [];
foreach ($lugares as $l) {
$lid = (int)$l['id'];
$stmt = $pdo->prepare("
SELECT t.id, t.paciente_nombre, t.inicio_lugar_at, ts.numero_orden
FROM turnero_turnos t
JOIN turnero_solicitudes ts ON ts.turno_id = t.id
WHERE t.sesion_id = ? AND t.lugar_destino_id = ?
AND t.inicio_lugar_at IS NOT NULL AND t.fin_lugar_at IS NULL
ORDER BY ts.numero_orden ASC
");
$stmt->execute([$sesionId, $lid]);
$grupos[] = [
'lugar_id' => $lid,
'lugar_nombre' => $l['nombre'],
'pacientes' => $stmt->fetchAll(PDO::FETCH_ASSOC),
];
}
jsonOk(['grupos' => $grupos]);
+116
View File
@@ -82,6 +82,16 @@ try {
$stmt->execute([(int)($_SESSION['admin_user']['id'] ?? 0)]);
$adminFirmaSvg = $stmt->fetchColumn() ?: null;
} catch (\Throwable $_) {}
// Especialidades (Ginecología, Pediatría) — excluir el lugar actual
$especialidades = [];
foreach ($lugares as $_el) {
$_en = mb_strtolower($_el['nombre']);
if ((str_contains($_en, 'ginecol') || str_contains($_en, 'pediatr'))
&& (int)$_el['id'] !== $lugarIdParam) {
$especialidades[] = $_el;
}
}
?>
<!DOCTYPE html>
<html lang="es">
@@ -148,6 +158,49 @@ try {
.cola-items { flex: 1; overflow-y: auto; padding: .5rem; }
/* ── Especialidades en cola ── */
.esp-toplinks {
display: flex; gap: .35rem; flex-wrap: wrap; flex-shrink: 0;
padding: .45rem .75rem; background: #fff;
border-bottom: 1px solid #f1f5f9;
}
.esp-toplink {
display: inline-flex; align-items: center; gap: .3rem;
padding: .2rem .6rem; border-radius: 99px; font-size: .75rem; font-weight: 600;
color: var(--esp-c, #64748b);
border: 1.5px solid color-mix(in srgb, var(--esp-c, #64748b) 30%, transparent);
background: color-mix(in srgb, var(--esp-c, #64748b) 8%, #fff);
text-decoration: none; white-space: nowrap; transition: background .12s;
}
.esp-toplink:hover { background: color-mix(in srgb, var(--esp-c) 18%, #fff); }
.esp-toplink .esp-cnt {
background: var(--esp-c); color: #fff; border-radius: 99px;
padding: 0 5px; font-size: .65rem; min-width: 16px; text-align: center;
}
#esp-panel { flex-shrink: 0; border-bottom: 2px solid #e2e8f0; }
.esp-grupo { padding: .35rem .6rem; }
.esp-grupo + .esp-grupo { border-top: 1px solid #f1f5f9; }
.esp-grupo-hdr {
display: flex; align-items: center; gap: .35rem;
font-size: .72rem; font-weight: 700; color: #475569; margin-bottom: .2rem;
}
.esp-grupo-count {
background: var(--esp-gc); color: #fff; border-radius: 99px; padding: 0 5px; font-size: .65rem;
}
.esp-grupo-ir {
margin-left: auto; font-size: .7rem; font-weight: 600;
color: var(--esp-gc); text-decoration: none;
}
.esp-grupo-ir:hover { text-decoration: underline; }
.esp-pac-row {
display: flex; align-items: center; gap: .4rem;
padding: .18rem .25rem; border-radius: 5px; font-size: .77rem;
text-decoration: none; color: #1e293b; transition: background .1s;
}
.esp-pac-row:hover { background: #f1f5f9; }
.esp-pac-orden { font-weight: 700; font-family: monospace; color: var(--esp-gc); font-size: .7rem; flex-shrink: 0; }
.esp-pac-hora { margin-left: auto; font-size: .68rem; color: #94a3b8; flex-shrink: 0; }
.cola-section-sep {
font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; font-weight: 700;
color: #94a3b8; padding: .3rem .4rem .15rem; margin-top: .25rem;
@@ -605,6 +658,24 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
<i class="fas fa-bell"></i>Llamar siguiente
</button>
</div>
<?php if ($especialidades): ?>
<div class="esp-toplinks">
<?php foreach ($especialidades as $_esp):
$_esGine = mb_stripos($_esp['nombre'], 'ginecol') !== false;
$_espC = $_esGine ? '#ec4899' : '#0ea5e9';
$_espI = $_esGine ? 'fa-venus' : 'fa-child';
?>
<a href="<?= BASE_URL ?>erp.php?m=turnero&v=lugar&lugar_id=<?= (int)$_esp['id'] ?>"
class="esp-toplink" style="--esp-c:<?= $_espC ?>">
<i class="fas <?= $_espI ?>"></i>
<?= htmlspecialchars($_esp['nombre']) ?>
<span class="esp-cnt" id="esp-cnt-<?= (int)$_esp['id'] ?>">—</span>
</a>
<?php endforeach; ?>
</div>
<div id="esp-panel" style="display:none"></div>
<?php endif; ?>
<div class="cola-items" id="cola-items">
<div class="cola-vacia">
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
@@ -928,6 +999,7 @@ 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 ?>';
const ESP_LUGARES = <?= json_encode(array_values($especialidades)) ?>;
// ── Arranque ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
@@ -937,7 +1009,9 @@ document.addEventListener('DOMContentLoaded', () => {
function iniciarPuesto() {
recuperarTurnoActivo();
cargarCola();
_cargarEspecialidades();
pollingColaId = setInterval(cargarCola, 7000);
setInterval(_cargarEspecialidades, 30000);
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
@@ -946,6 +1020,7 @@ function iniciarPuesto() {
pollingColaId = pollingConsentId = null;
} else if (lugarId) {
cargarCola();
_cargarEspecialidades();
pollingColaId = setInterval(cargarCola, 7000);
}
});
@@ -2321,6 +2396,47 @@ function mostrarToast(msg, type = 'info', duration = 2500) {
function mostrarError(msg) { mostrarToast(msg, 'error', 4000); }
function mostrarLlamando(cod) { mostrarToast('Llamando turno ' + cod + '…', 'info', 2000); }
async function _cargarEspecialidades() {
if (!ESP_LUGARES.length) return;
try {
const qs = ESP_LUGARES.map(l => `ids[]=${l.id}`).join('&');
const res = await fetch(`${API}get_especialidades.php?${qs}`);
const j = await res.json();
if (j.ok) _renderEspecialidades(j.grupos);
} catch {}
}
function _renderEspecialidades(grupos) {
const panel = document.getElementById('esp-panel');
if (!panel) return;
let html = '';
for (const g of grupos) {
const esGine = g.lugar_nombre.toLowerCase().includes('ginecol');
const color = esGine ? '#ec4899' : '#0ea5e9';
const icon = esGine ? 'fa-venus' : 'fa-child';
const url = `${BASE_WA}erp.php?m=turnero&v=lugar&lugar_id=${g.lugar_id}`;
const cnt = document.getElementById(`esp-cnt-${g.lugar_id}`);
if (cnt) cnt.textContent = g.pacientes.length;
if (!g.pacientes.length) continue;
html += `<div class="esp-grupo" style="--esp-gc:${color}">
<div class="esp-grupo-hdr">
<i class="fas ${icon}" style="color:${color}"></i>
${escHtml(g.lugar_nombre)}
<span class="esp-grupo-count">${g.pacientes.length}</span>
<a href="${url}" class="esp-grupo-ir">Ir <i class="fas fa-arrow-right fa-xs"></i></a>
</div>
${g.pacientes.map(p => `
<a href="${url}" class="esp-pac-row">
<span class="esp-pac-orden">#${p.numero_orden}</span>
<span>${escHtml(p.paciente_nombre)}</span>
<span class="esp-pac-hora">${formatHora(p.inicio_lugar_at)}</span>
</a>`).join('')}
</div>`;
}
panel.innerHTML = html;
panel.style.display = html ? '' : 'none';
}
/* ── Ocupación exclusiva del puesto ──────────────────────────── */
(function(_ID, _NOMBRE, _VIEW, _PARAM) {
if (!_ID) return;