feat(bandeja): vistos/por-revisar, filtro nombre+examen, marcar como visto

- Reemplaza activos/cerrados por pendientes (por revisar) / vistos
- Columna bandeja_visto_at en turnero_turnos; API marcar_visto.php
- Botón "Marcar como visto" → mueve paciente a sección inferior
- Filtro en tiempo real por nombre y por código de examen (GROUP_CONCAT)
- URL de consentimientos: relativa (consent.php?token=...)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-27 21:29:52 -05:00
co-authored by Claude Sonnet 4.6
parent 6c54674795
commit 368c4c8a5a
3 changed files with 215 additions and 267 deletions
+13 -9
View File
@@ -19,6 +19,7 @@ if (isset($_GET['turno_id'])) {
$turno = $pdo->prepare(" $turno = $pdo->prepare("
SELECT t.id, t.codigo, t.estado, t.paciente_nombre, t.inicio_lugar_at, t.fin_lugar_at, SELECT t.id, t.codigo, t.estado, t.paciente_nombre, t.inicio_lugar_at, t.fin_lugar_at,
t.bandeja_visto_at,
ts.id AS sol_id, ts.numero_orden, ts.id AS sol_id, ts.numero_orden,
p.tipo_documento, p.numero_documento, p.fecha_nacimiento, p.telefono, p.tipo_documento, p.numero_documento, p.fecha_nacimiento, p.telefono,
l.nombre AS lugar_nombre l.nombre AS lugar_nombre
@@ -92,21 +93,24 @@ if (isset($_GET['turno_id'])) {
// ── LISTA ───────────────────────────────────────────────────── // ── LISTA ─────────────────────────────────────────────────────
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT t.id, t.codigo, t.paciente_nombre, t.estado, SELECT t.id, t.codigo, t.paciente_nombre, t.estado,
t.inicio_lugar_at, t.fin_lugar_at, t.inicio_lugar_at, t.fin_lugar_at, t.bandeja_visto_at,
ts.numero_orden, ts.numero_orden,
l.nombre AS lugar_nombre l.nombre AS lugar_nombre,
GROUP_CONCAT(et.codigo ORDER BY et.codigo SEPARATOR ' ') AS examenes_txt
FROM turnero_turnos t FROM turnero_turnos t
JOIN turnero_solicitudes ts ON ts.turno_id = t.id JOIN turnero_solicitudes ts ON ts.turno_id = t.id
JOIN turnero_lugares l ON l.id = t.lugar_destino_id JOIN turnero_lugares l ON l.id = t.lugar_destino_id
LEFT JOIN turnero_examen_items tei ON tei.solicitud_id = ts.id
LEFT JOIN exam_tipos et ON et.id = tei.exam_tipo_id
WHERE t.sesion_id = ? AND t.inicio_lugar_at IS NOT NULL WHERE t.sesion_id = ? AND t.inicio_lugar_at IS NOT NULL
GROUP BY t.id
ORDER BY ts.numero_orden ASC ORDER BY ts.numero_orden ASC
"); ");
$stmt->execute([$sesionId]); $stmt->execute([$sesionId]);
$todos = $stmt->fetchAll(PDO::FETCH_ASSOC); $todos = $stmt->fetchAll(PDO::FETCH_ASSOC);
$cerradosEstados = ['finalizado', 'ausente', 'cancelado']; $pendientes = array_values(array_filter($todos, fn($t) => $t['bandeja_visto_at'] === null));
$activos = array_values(array_filter($todos, fn($t) => !in_array($t['estado'], $cerradosEstados))); $vistos = array_values(array_filter($todos, fn($t) => $t['bandeja_visto_at'] !== null));
$cerrados = array_values(array_filter($todos, fn($t) => in_array($t['estado'], $cerradosEstados)));
$stmtMp = $pdo->prepare(" $stmtMp = $pdo->prepare("
SELECT COUNT(*) FROM turnero_muestras tm SELECT COUNT(*) FROM turnero_muestras tm
@@ -118,12 +122,12 @@ $stmtMp->execute([$sesionId]);
$muestrasPendientes = (int)$stmtMp->fetchColumn(); $muestrasPendientes = (int)$stmtMp->fetchColumn();
jsonOk([ jsonOk([
'activos' => $activos, 'pendientes' => $pendientes,
'cerrados' => $cerrados, 'vistos' => $vistos,
'stats' => [ 'stats' => [
'total' => count($todos), 'total' => count($todos),
'activos' => count($activos), 'pendientes' => count($pendientes),
'cerrados' => count($cerrados), 'vistos' => count($vistos),
'muestras_pendientes' => $muestrasPendientes, 'muestras_pendientes' => $muestrasPendientes,
], ],
]); ]);
+16
View File
@@ -0,0 +1,16 @@
<?php
/**
* modules/turnero/api/marcar_visto.php
* POST {turno_id} → marca el turno como visto en la bandeja
*/
require_once __DIR__ . '/_helpers.php';
requireTurnero();
requireMethod('POST');
$input = inputJson();
$turnoId = (int)($input['turno_id'] ?? 0);
if (!$turnoId) jsonError('turno_id requerido');
db()->prepare('UPDATE turnero_turnos SET bandeja_visto_at = NOW() WHERE id = ?')->execute([$turnoId]);
jsonOk([], 'Marcado como visto');
+167 -239
View File
@@ -2,19 +2,19 @@
Layout::open('Bandeja del día', 'fas fa-layer-group'); Layout::open('Bandeja del día', 'fas fa-layer-group');
?> ?>
<style> <style>
/* ── Layout principal ───────────────────────────────────────── */ /* ── Layout ──────────────────────────────────────────────────── */
.bnd-wrap { .bnd-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 60px); /* 60px = altura navbar ERP */ height: calc(100vh - 60px);
overflow: hidden; overflow: hidden;
background: #f8fafc; background: #f8fafc;
} }
.bnd-topbar { .bnd-topbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
padding: 10px 18px; padding: 8px 16px;
background: #fff; background: #fff;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
flex-shrink: 0; flex-shrink: 0;
@@ -25,6 +25,7 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
margin-right: 4px; margin-right: 4px;
white-space: nowrap;
} }
.bnd-chip { .bnd-chip {
display: inline-flex; display: inline-flex;
@@ -35,10 +36,11 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 600; font-weight: 600;
line-height: 1.6; line-height: 1.6;
white-space: nowrap;
} }
.bnd-chip-total { background:#ede9fe; color:#5b21b6; } .bnd-chip-total { background:#ede9fe; color:#5b21b6; }
.bnd-chip-activos { background:#dcfce7; color:#15803d; } .bnd-chip-pend { background:#fef3c7; color:#92400e; }
.bnd-chip-cerr { background:#f1f5f9; color:#475569; } .bnd-chip-vis { background:#f1f5f9; color:#475569; }
.bnd-chip-mp { background:#fff7ed; color:#c2410c; } .bnd-chip-mp { background:#fff7ed; color:#c2410c; }
.bnd-refresh-btn { .bnd-refresh-btn {
margin-left: auto; margin-left: auto;
@@ -52,74 +54,75 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
white-space: nowrap;
} }
.bnd-refresh-btn:hover { background:#f1f5f9; } .bnd-refresh-btn:hover { background:#f1f5f9; }
/* ── Cuerpo split ───────────────────────────────────────────── */ /* ── Cuerpo ──────────────────────────────────────────────────── */
.bnd-body { .bnd-body { display: flex; flex: 1; overflow: hidden; }
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Lista izquierda ────────────────────────────────────────── */ /* ── Lista izquierda ────────────────────────────────────────── */
.bnd-list { .bnd-list {
width: 280px; width: 290px;
flex-shrink: 0; flex-shrink: 0;
border-right: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0;
overflow-y: auto; display: flex;
flex-direction: column;
background: #fff; background: #fff;
} }
.bnd-search-box {
padding: 8px 10px;
border-bottom: 1px solid #f1f5f9;
flex-shrink: 0;
}
.bnd-search-box input {
width: 100%;
border: 1.5px solid #e2e8f0;
border-radius: 7px;
padding: 5px 10px;
font-size: 0.8rem;
outline: none;
font-family: inherit;
box-sizing: border-box;
}
.bnd-search-box input:focus { border-color: #7c3aed; }
.bnd-list-scroll { flex: 1; overflow-y: auto; }
.bnd-section-hdr { .bnd-section-hdr {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 1;
padding: 6px 14px; padding: 5px 12px;
font-size: 0.7rem; font-size: 0.68rem;
font-weight: 700; font-weight: 700;
letter-spacing: .06em; letter-spacing: .07em;
text-transform: uppercase; text-transform: uppercase;
background: #f8fafc; background: #f8fafc;
color: #64748b; color: #64748b;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
border-top: 1px solid #e2e8f0;
} }
.bnd-card { .bnd-card {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
padding: 0;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9;
transition: background .12s; transition: background .1s;
position: relative;
} }
.bnd-card:hover { background: #f8fafc; } .bnd-card:hover { background: #f8fafc; }
.bnd-card.selected { background: #eef2ff; } .bnd-card.selected { background: #eef2ff; }
.bnd-card-bar { .bnd-card-bar { width: 3px; flex-shrink: 0; }
width: 3px; .bnd-card-bar.pendiente { background: #f59e0b; }
flex-shrink: 0; .bnd-card-bar.visto { background: #cbd5e1; }
border-radius: 0; .bnd-card-inner { padding: 8px 10px; min-width: 0; flex: 1; }
}
.bnd-card-bar.activo { background: #22c55e; }
.bnd-card-bar.cerrado { background: #cbd5e1; }
.bnd-card-inner {
padding: 9px 12px;
min-width: 0;
flex: 1;
}
.bnd-card-top { .bnd-card-top {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 6px; gap: 5px;
margin-bottom: 2px; margin-bottom: 1px;
}
.bnd-orden {
font-size: 0.7rem;
font-weight: 700;
color: #7c3aed;
white-space: nowrap;
} }
.bnd-orden { font-size: 0.68rem; font-weight: 700; color: #7c3aed; white-space: nowrap; }
.bnd-nombre { .bnd-nombre {
font-size: 0.82rem; font-size: 0.81rem;
font-weight: 600; font-weight: 600;
color: #1e293b; color: #1e293b;
white-space: nowrap; white-space: nowrap;
@@ -127,32 +130,13 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: 0; min-width: 0;
} }
.bnd-card.cerrado .bnd-nombre { color: #94a3b8; } .bnd-card.visto .bnd-nombre { color: #94a3b8; }
.bnd-card-bot { .bnd-card-bot { display: flex; align-items: center; justify-content: space-between; }
display: flex; .bnd-lugar-badge { font-size: 0.67rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
align-items: center; .bnd-hora { font-size: 0.67rem; color: #94a3b8; white-space: nowrap; margin-left: 4px; }
justify-content: space-between;
}
.bnd-lugar-badge {
font-size: 0.68rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bnd-hora {
font-size: 0.68rem;
color: #94a3b8;
white-space: nowrap;
margin-left: 4px;
}
/* ── Detalle derecha ────────────────────────────────────────── */ /* ── Detalle derecha ────────────────────────────────────────── */
.bnd-detail { .bnd-detail { flex: 1; overflow-y: auto; }
flex: 1;
overflow-y: auto;
padding: 0;
}
.bnd-empty { .bnd-empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -172,35 +156,18 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
z-index: 2; z-index: 2;
background: #fff; background: #fff;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
padding: 14px 20px 12px; padding: 12px 20px 10px;
}
.bnd-dh-nombre {
font-size: 1.05rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 4px;
}
.bnd-dh-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.bnd-dh-meta span {
font-size: 0.78rem;
color: #64748b;
} }
.bnd-dh-nombre { font-size: 1.05rem; font-weight: 700; color: #1e293b; margin-bottom: 3px; }
.bnd-dh-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.bnd-dh-meta span { font-size: 0.77rem; color: #64748b; }
.bnd-dh-meta .sep { color: #cbd5e1; } .bnd-dh-meta .sep { color: #cbd5e1; }
.bnd-dh-actions { .bnd-dh-actions { display: flex; gap: 8px; margin-top: 9px; flex-wrap: wrap; }
display: flex;
gap: 8px;
margin-top: 10px;
}
.bnd-btn { .bnd-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 5px 14px; padding: 5px 13px;
border-radius: 6px; border-radius: 6px;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
@@ -210,104 +177,65 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
} }
.bnd-btn-pdf { border-color:#3b82f6; color:#2563eb; background:#fff; } .bnd-btn-pdf { border-color:#3b82f6; color:#2563eb; background:#fff; }
.bnd-btn-pdf:hover { background:#eff6ff; } .bnd-btn-pdf:hover { background:#eff6ff; }
.bnd-btn-cerrar { border-color:#22c55e; color:#15803d; background:#fff; } .bnd-btn-visto { border-color:#7c3aed; color:#5b21b6; background:#fff; }
.bnd-btn-cerrar:hover { background:#f0fdf4; } .bnd-btn-visto:hover { background:#f5f3ff; }
.bnd-btn-cerrado { border-color:#cbd5e1; color:#94a3b8; background:#f8fafc; cursor:default; } .bnd-btn-ya-visto{ border-color:#cbd5e1; color:#94a3b8; background:#f8fafc; cursor:default; }
/* Sections */ /* Secciones */
.bnd-sec { .bnd-sec { padding: 14px 20px; border-bottom: 1px solid #f1f5f9; }
padding: 14px 20px;
border-bottom: 1px solid #f1f5f9;
}
.bnd-sec-title { .bnd-sec-title {
font-size: 0.72rem; font-size: 0.7rem;
font-weight: 700; font-weight: 700;
letter-spacing: .07em; letter-spacing: .07em;
text-transform: uppercase; text-transform: uppercase;
color: #94a3b8; color: #94a3b8;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* Exámenes */
/* Exámenes chips */
.bnd-cat { margin-bottom: 8px; } .bnd-cat { margin-bottom: 8px; }
.bnd-cat-name { .bnd-cat-name { font-size: 0.71rem; font-weight: 600; color: #475569; margin-bottom: 4px; }
font-size: 0.72rem;
font-weight: 600;
color: #475569;
margin-bottom: 4px;
}
.bnd-chips { display: flex; flex-wrap: wrap; gap: 4px; } .bnd-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.bnd-exam-chip { .bnd-exam-chip {
background: #f1f5f9; background: #f1f5f9;
color: #334155; color: #334155;
font-size: 0.72rem; font-size: 0.71rem;
font-weight: 600; font-weight: 600;
padding: 2px 8px; padding: 2px 8px;
border-radius: 12px; border-radius: 12px;
} }
/* Muestras */ /* Muestras */
.bnd-muestra { .bnd-muestra { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 0.82rem; }
display: flex; .bnd-m-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
align-items: center;
gap: 8px;
padding: 5px 0;
font-size: 0.82rem;
}
.bnd-m-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.bnd-m-dot.pendiente { background: #f59e0b; } .bnd-m-dot.pendiente { background: #f59e0b; }
.bnd-m-dot.recibida { background: #22c55e; } .bnd-m-dot.recibida { background: #22c55e; }
.bnd-m-dot.rechazada { background: #ef4444; } .bnd-m-dot.rechazada { background: #ef4444; }
.bnd-m-label { font-weight: 600; color: #1e293b; } .bnd-m-label { font-weight: 600; color: #1e293b; }
.bnd-m-estado { color: #64748b; font-size: 0.75rem; } .bnd-m-estado { color: #64748b; font-size: 0.74rem; }
.bnd-m-rechazado { color: #ef4444; font-size: 0.73rem; } .bnd-m-rechazado { color: #ef4444; font-size: 0.72rem; }
/* Consentimientos */ /* Consentimientos */
.bnd-consent { .bnd-consent { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 0.82rem; }
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
font-size: 0.82rem;
}
.bnd-consent-ico { font-size: 0.85rem; }
.bnd-consent-ico.firmado { color: #22c55e; } .bnd-consent-ico.firmado { color: #22c55e; }
.bnd-consent-ico.pendiente{ color: #f59e0b; } .bnd-consent-ico.pendiente{ color: #f59e0b; }
.bnd-consent a { font-size: 0.75rem; color: #3b82f6; margin-left: 4px; } .bnd-consent a { font-size: 0.74rem; color: #3b82f6; margin-left: 4px; text-decoration: none; }
.bnd-consent a:hover { text-decoration: underline; }
/* Comentarios timeline */ /* Comentarios */
.bnd-com-list { display: flex; flex-direction: column; gap: 8px; } .bnd-com-list { display: flex; flex-direction: column; gap: 7px; }
.bnd-com { .bnd-com { background: #f8fafc; border-radius: 8px; padding: 8px 12px; font-size: 0.82rem; }
background: #f8fafc; .bnd-com-header { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
border-radius: 8px;
padding: 8px 12px;
font-size: 0.82rem;
}
.bnd-com-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 3px;
}
.bnd-com-tipo { .bnd-com-tipo {
font-size: 0.68rem; font-size: 0.66rem;
font-weight: 700; font-weight: 700;
letter-spacing: .04em; letter-spacing: .04em;
padding: 1px 6px; padding: 1px 6px;
border-radius: 10px; border-radius: 10px;
text-transform: uppercase; text-transform: uppercase;
} }
.bnd-com-tipo.recepcion { background: #dbeafe; color: #1d4ed8; } .bnd-com-tipo.recepcion { background:#dbeafe; color:#1d4ed8; }
.bnd-com-tipo.muestras { background: #dcfce7; color: #15803d; } .bnd-com-tipo.muestras { background:#dcfce7; color:#15803d; }
.bnd-com-tipo.general { background: #fef3c7; color: #92400e; } .bnd-com-tipo.general { background:#fef3c7; color:#92400e; }
.bnd-com-user { font-weight: 600; color: #1e293b; } .bnd-com-user { font-weight: 600; color: #1e293b; }
.bnd-com-date { font-size: 0.7rem; color: #94a3b8; margin-left: auto; } .bnd-com-date { font-size: 0.7rem; color: #94a3b8; margin-left: auto; }
.bnd-com-text { color: #334155; } .bnd-com-text { color: #334155; }
/* Agregar comentario */ /* Agregar comentario */
.bnd-add-com { display: flex; gap: 8px; align-items: flex-end; } .bnd-add-com { display: flex; gap: 8px; align-items: flex-end; }
.bnd-add-com textarea { .bnd-add-com textarea {
@@ -318,7 +246,7 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
font-size: 0.82rem; font-size: 0.82rem;
resize: none; resize: none;
font-family: inherit; font-family: inherit;
height: 56px; height: 52px;
outline: none; outline: none;
transition: border-color .15s; transition: border-color .15s;
} }
@@ -328,18 +256,17 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
color: #fff; color: #fff;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 10px 16px; padding: 10px 14px;
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
height: 56px; height: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
} }
.bnd-add-com button:hover { background: #6d28d9; } .bnd-add-com button:hover { background: #6d28d9; }
.bnd-add-com button:disabled { background: #c4b5fd; cursor:default; } .bnd-add-com button:disabled { background: #c4b5fd; cursor:default; }
/* Toast */ /* Toast */
.bnd-toast { .bnd-toast {
position: fixed; position: fixed;
@@ -357,8 +284,7 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
pointer-events: none; pointer-events: none;
} }
.bnd-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .bnd-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ── Print ── */
/* ── Print ──────────────────────────────────────────────────── */
@media print { @media print {
.bnd-list, .bnd-topbar, .bnd-dh-actions, .bnd-list, .bnd-topbar, .bnd-dh-actions,
.bnd-add-com, nav, .sidebar, .erp-sidebar, .bnd-add-com, nav, .sidebar, .erp-sidebar,
@@ -370,12 +296,11 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
</style> </style>
<div class="bnd-wrap"> <div class="bnd-wrap">
<!-- Stats bar --> <div class="bnd-topbar">
<div class="bnd-topbar" id="bnd-topbar">
<span class="bnd-title"><i class="fas fa-layer-group"></i> Bandeja del día</span> <span class="bnd-title"><i class="fas fa-layer-group"></i> Bandeja del día</span>
<span class="bnd-chip bnd-chip-total" id="st-total"><i class="fas fa-users"></i> —</span> <span class="bnd-chip bnd-chip-total" id="st-total"><i class="fas fa-users"></i> —</span>
<span class="bnd-chip bnd-chip-activos" id="st-activos"><i class="fas fa-circle" style="font-size:.55rem"></i> Activos: —</span> <span class="bnd-chip bnd-chip-pend" id="st-pend"><i class="fas fa-clock"></i> Por revisar: —</span>
<span class="bnd-chip bnd-chip-cerr" id="st-cerr"><i class="fas fa-check-double"></i> Cerrados: —</span> <span class="bnd-chip bnd-chip-vis" id="st-vis"><i class="fas fa-eye"></i> Vistos: —</span>
<span class="bnd-chip bnd-chip-mp" id="st-mp"><i class="fas fa-exclamation-triangle"></i> Muestras pend.: —</span> <span class="bnd-chip bnd-chip-mp" id="st-mp"><i class="fas fa-exclamation-triangle"></i> Muestras pend.: —</span>
<button class="bnd-refresh-btn" onclick="_bndRefrescarLista()"> <button class="bnd-refresh-btn" onclick="_bndRefrescarLista()">
<i class="fas fa-sync-alt" id="bnd-spin"></i> Actualizar <i class="fas fa-sync-alt" id="bnd-spin"></i> Actualizar
@@ -383,12 +308,15 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
</div> </div>
<div class="bnd-body"> <div class="bnd-body">
<!-- Lista --> <div class="bnd-list">
<div class="bnd-list" id="bnd-list"> <div class="bnd-search-box">
<input type="text" id="bnd-filtro" placeholder="Buscar por nombre o examen…" oninput="_bndFiltrar()" autocomplete="off">
</div>
<div class="bnd-list-scroll" id="bnd-list-scroll">
<div class="bnd-section-hdr">Cargando...</div> <div class="bnd-section-hdr">Cargando...</div>
</div> </div>
</div>
<!-- Detalle -->
<div class="bnd-detail" id="bnd-detail"> <div class="bnd-detail" id="bnd-detail">
<div class="bnd-empty"> <div class="bnd-empty">
<i class="fas fa-hand-pointer"></i> <i class="fas fa-hand-pointer"></i>
@@ -403,17 +331,14 @@ Layout::open('Bandeja del día', 'fas fa-layer-group');
<script> <script>
const BND_API = 'modules/turnero/api/get_bandeja.php'; const BND_API = 'modules/turnero/api/get_bandeja.php';
const COM_API = 'modules/turnero/api/comentarios.php'; const COM_API = 'modules/turnero/api/comentarios.php';
const BASE_URL = 'https://erp.laboratorioximenacaicedo.com/'; const VISTO_API= 'modules/turnero/api/marcar_visto.php';
let _bndTurnoId = null; let _bndTurnoId = null;
let _bndInterval = null; let _bndAllData = { pendientes: [], vistos: [] };
let _bndListData = { activos: [], cerrados: [] };
// ── Utilidades ───────────────────────────────────────────────
function _hora(dt) { function _hora(dt) {
if (!dt) return ''; if (!dt) return '';
const d = new Date(dt.replace(' ', 'T')); return new Date(dt.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'});
return d.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
} }
function _toast(msg, ms = 2200) { function _toast(msg, ms = 2200) {
const t = document.getElementById('bnd-toast'); const t = document.getElementById('bnd-toast');
@@ -427,25 +352,36 @@ async function _bndRefrescarLista() {
const spin = document.getElementById('bnd-spin'); const spin = document.getElementById('bnd-spin');
spin.style.transition = 'transform .4s'; spin.style.transition = 'transform .4s';
spin.style.transform = 'rotate(360deg)'; spin.style.transform = 'rotate(360deg)';
setTimeout(() => { spin.style.transition=''; spin.style.transform=''; }, 400); setTimeout(()=>{ spin.style.transition=''; spin.style.transform=''; }, 400);
const res = await fetch(BND_API).then(r => r.json()).catch(() => null); const res = await fetch(BND_API).then(r=>r.json()).catch(()=>null);
if (!res?.ok) return; if (!res?.ok) return;
_bndListData = res; _bndAllData = res;
const s = res.stats; const s = res.stats;
document.getElementById('st-total').innerHTML = `<i class="fas fa-users"></i> Total: ${s.total}`; document.getElementById('st-total').innerHTML = `<i class="fas fa-users"></i> Total: ${s.total}`;
document.getElementById('st-activos').innerHTML = `<i class="fas fa-circle" style="font-size:.55rem"></i> Activos: ${s.activos}`; document.getElementById('st-pend').innerHTML = `<i class="fas fa-clock"></i> Por revisar: ${s.pendientes}`;
document.getElementById('st-cerr').innerHTML = `<i class="fas fa-check-double"></i> Cerrados: ${s.cerrados}`; document.getElementById('st-vis').innerHTML = `<i class="fas fa-eye"></i> Vistos: ${s.vistos}`;
document.getElementById('st-mp').innerHTML = `<i class="fas fa-exclamation-triangle"></i> Muestras pend.: ${s.muestras_pendientes}`; document.getElementById('st-mp').innerHTML = `<i class="fas fa-exclamation-triangle"></i> Muestras pend.: ${s.muestras_pendientes}`;
_renderLista(res.activos, res.cerrados); _bndFiltrar();
} }
function _renderLista(activos, cerrados) { function _bndFiltrar() {
const el = document.getElementById('bnd-list'); const q = (document.getElementById('bnd-filtro')?.value || '').toLowerCase().trim();
let html = ''; const match = t =>
!q ||
(t.paciente_nombre || '').toLowerCase().includes(q) ||
(t.examenes_txt || '').toLowerCase().includes(q);
_renderLista(
_bndAllData.pendientes?.filter(match) || [],
_bndAllData.vistos?.filter(match) || []
);
}
function _renderLista(pendientes, vistos) {
const el = document.getElementById('bnd-list-scroll');
const card = (t, tipo) => { const card = (t, tipo) => {
const sel = t.id === _bndTurnoId ? ' selected' : ''; const sel = t.id === _bndTurnoId ? ' selected' : '';
const orden = t.numero_orden || t.codigo; const orden = t.numero_orden || t.codigo;
@@ -458,20 +394,20 @@ function _renderLista(activos, cerrados) {
<span class="bnd-nombre">${nombre}</span> <span class="bnd-nombre">${nombre}</span>
</div> </div>
<div class="bnd-card-bot"> <div class="bnd-card-bot">
<span class="bnd-lugar-badge">${t.lugar_nombre || ''}</span> <span class="bnd-lugar-badge">${t.lugar_nombre||''}</span>
<span class="bnd-hora">${_hora(t.inicio_lugar_at)}</span> <span class="bnd-hora">${_hora(t.inicio_lugar_at)}</span>
</div> </div>
</div> </div>
</div>`; </div>`;
}; };
html += `<div class="bnd-section-hdr">Activos (${activos.length})</div>`; let html = `<div class="bnd-section-hdr">Por revisar (${pendientes.length})</div>`;
if (!activos.length) html += `<div style="padding:12px 14px;font-size:.78rem;color:#94a3b8">Sin pacientes activos</div>`; if (!pendientes.length) html += `<div style="padding:10px 12px;font-size:.77rem;color:#94a3b8">Sin pacientes pendientes</div>`;
activos.forEach(t => html += card(t, 'activo')); pendientes.forEach(t => html += card(t, 'pendiente'));
html += `<div class="bnd-section-hdr" style="top:0">Cerrados (${cerrados.length})</div>`; html += `<div class="bnd-section-hdr">Vistos (${vistos.length})</div>`;
if (!cerrados.length) html += `<div style="padding:12px 14px;font-size:.78rem;color:#94a3b8">Ninguno cerrado</div>`; if (!vistos.length) html += `<div style="padding:10px 12px;font-size:.77rem;color:#94a3b8">Ninguno visto aún</div>`;
cerrados.forEach(t => html += card(t, 'cerrado')); vistos.forEach(t => html += card(t, 'visto'));
el.innerHTML = html; el.innerHTML = html;
} }
@@ -479,16 +415,14 @@ function _renderLista(activos, cerrados) {
// ── Detalle ─────────────────────────────────────────────────── // ── Detalle ───────────────────────────────────────────────────
async function _bndVerDetalle(turnoId) { async function _bndVerDetalle(turnoId) {
_bndTurnoId = turnoId; _bndTurnoId = turnoId;
// Resaltar en lista
document.querySelectorAll('.bnd-card').forEach(c => c.classList.remove('selected'));
document.querySelectorAll('.bnd-card').forEach(c => { document.querySelectorAll('.bnd-card').forEach(c => {
if (c.onclick?.toString().includes(`${turnoId}`)) c.classList.add('selected'); c.classList.toggle('selected', c.onclick?.toString().includes(`${turnoId}`));
}); });
const det = document.getElementById('bnd-detail'); const det = document.getElementById('bnd-detail');
det.innerHTML = `<div class="bnd-empty"><i class="fas fa-spinner fa-spin"></i><p>Cargando...</p></div>`; det.innerHTML = `<div class="bnd-empty"><i class="fas fa-spinner fa-spin"></i><p>Cargando...</p></div>`;
const res = await fetch(`${BND_API}?turno_id=${turnoId}`).then(r => r.json()).catch(() => null); const res = await fetch(`${BND_API}?turno_id=${turnoId}`).then(r=>r.json()).catch(()=>null);
if (!res?.ok) { det.innerHTML = `<div class="bnd-empty"><i class="fas fa-times-circle"></i><p>Error al cargar</p></div>`; return; } if (!res?.ok) { det.innerHTML = `<div class="bnd-empty"><i class="fas fa-times-circle"></i><p>Error al cargar</p></div>`; return; }
_renderDetalle(res); _renderDetalle(res);
@@ -496,38 +430,35 @@ async function _bndVerDetalle(turnoId) {
function _renderDetalle(d) { function _renderDetalle(d) {
const t = d.turno; const t = d.turno;
const isCerrado = ['finalizado','ausente','cancelado'].includes(t.estado); const yaVisto= !!t.bandeja_visto_at;
const orden = t.numero_orden || t.codigo;
// Header
let hdr = `<div class="bnd-dh"> let hdr = `<div class="bnd-dh">
<div class="bnd-dh-nombre">${t.paciente_nombre || '—'}</div> <div class="bnd-dh-nombre">${t.paciente_nombre||'—'}</div>
<div class="bnd-dh-meta"> <div class="bnd-dh-meta">
<span><b>${orden}</b></span> <span><b>${t.numero_orden||t.codigo}</b></span>
<span class="sep">·</span> <span class="sep">·</span>
<span>${t.lugar_nombre || ''}</span> <span>${t.lugar_nombre||''}</span>
<span class="sep">·</span> <span class="sep">·</span>
<span>${_hora(t.inicio_lugar_at)}</span> <span>${_hora(t.inicio_lugar_at)}</span>
${t.fin_lugar_at ? `<span class="sep">·</span><span>Cerrado ${_hora(t.fin_lugar_at)}</span>` : ''}
</div> </div>
<div class="bnd-dh-actions"> <div class="bnd-dh-actions">
<button class="bnd-btn bnd-btn-pdf" onclick="window.print()"><i class="fas fa-print"></i> PDF</button>`; <button class="bnd-btn bnd-btn-pdf" onclick="window.print()"><i class="fas fa-print"></i> PDF</button>`;
if (isCerrado) { if (yaVisto) {
hdr += `<button class="bnd-btn bnd-btn-cerrado" disabled><i class="fas fa-check"></i> Cerrado</button>`; hdr += `<button class="bnd-btn bnd-btn-ya-visto" disabled><i class="fas fa-eye"></i> Visto ${_hora(t.bandeja_visto_at)}</button>`;
} else { } else {
hdr += `<button class="bnd-btn bnd-btn-cerrar" onclick="_bndCerrar(${t.id})"><i class="fas fa-check-circle"></i> Cerrar</button>`; hdr += `<button class="bnd-btn bnd-btn-visto" onclick="_bndMarcarVisto(${t.id})"><i class="fas fa-eye"></i> Marcar como visto</button>`;
} }
hdr += `</div></div>`; hdr += `</div></div>`;
// Exámenes // Exámenes
let secEx = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-vial"></i> Exámenes</div>`; let secEx = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-vial"></i> Exámenes</div>`;
const cats = Object.keys(d.examenes_por_cat); const cats = Object.keys(d.examenes_por_cat||{});
if (!cats.length) { if (!cats.length) {
secEx += `<span style="color:#94a3b8;font-size:.82rem">Sin exámenes registrados</span>`; secEx += `<span style="color:#94a3b8;font-size:.82rem">Sin exámenes registrados</span>`;
} else { } else {
cats.forEach(cat => { cats.forEach(cat => {
const chips = d.examenes_por_cat[cat].map(c => `<span class="bnd-exam-chip">${c}</span>`).join(''); const chips = d.examenes_por_cat[cat].map(c=>`<span class="bnd-exam-chip">${c}</span>`).join('');
secEx += `<div class="bnd-cat"><div class="bnd-cat-name">${cat}</div><div class="bnd-chips">${chips}</div></div>`; secEx += `<div class="bnd-cat"><div class="bnd-cat-name">${cat}</div><div class="bnd-chips">${chips}</div></div>`;
}); });
} }
@@ -535,16 +466,16 @@ function _renderDetalle(d) {
// Muestras // Muestras
let secM = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-tint"></i> Muestras</div>`; let secM = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-tint"></i> Muestras</div>`;
if (!d.muestras.length) { if (!d.muestras?.length) {
secM += `<span style="color:#94a3b8;font-size:.82rem">Sin muestras registradas</span>`; secM += `<span style="color:#94a3b8;font-size:.82rem">Sin muestras registradas</span>`;
} else { } else {
d.muestras.forEach(m => { d.muestras.forEach(m => {
const rechazo = m.estado === 'rechazada' && m.motivo_rechazo const rechazo = m.estado==='rechazada'&&m.motivo_rechazo
? `<span class="bnd-m-rechazado"> · ${m.motivo_rechazo}</span>` : ''; ? `<span class="bnd-m-rechazado"> · ${m.motivo_rechazo}</span>` : '';
secM += `<div class="bnd-muestra"> secM += `<div class="bnd-muestra">
<div class="bnd-m-dot ${m.estado}"></div> <div class="bnd-m-dot ${m.estado}"></div>
<span class="bnd-m-label">${m.tipo_muestra}</span> <span class="bnd-m-label">${m.tipo_muestra}</span>
<span class="bnd-m-estado">${m.estado}${m.recibida_at ? ' · ' + _hora(m.recibida_at) : ''}</span> <span class="bnd-m-estado">${m.estado}${m.recibida_at?' · '+_hora(m.recibida_at):''}</span>
${rechazo} ${rechazo}
</div>`; </div>`;
}); });
@@ -553,18 +484,18 @@ function _renderDetalle(d) {
// Consentimientos // Consentimientos
let secC = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-file-signature"></i> Consentimientos</div>`; let secC = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-file-signature"></i> Consentimientos</div>`;
if (!d.consentimientos.length) { if (!d.consentimientos?.length) {
secC += `<span style="color:#94a3b8;font-size:.82rem">Sin consentimientos</span>`; secC += `<span style="color:#94a3b8;font-size:.82rem">Sin consentimientos</span>`;
} else { } else {
d.consentimientos.forEach(c => { d.consentimientos.forEach(c => {
const esFirmado = c.estado === 'firmado'; const esFirmado = c.estado==='firmado';
const ico = esFirmado ? 'fas fa-check-circle firmado' : 'fas fa-clock pendiente'; const ico = esFirmado ? 'firmado fas fa-check-circle' : 'pendiente fas fa-clock';
const link = c.token const link = c.token
? `<a href="${BASE_URL}consent.php?token=${c.token}" target="_blank"><i class="fas fa-external-link-alt"></i> Ver</a>` : ''; ? `<a href="consent.php?token=${c.token}" target="_blank"><i class="fas fa-external-link-alt"></i> Ver</a>` : '';
secC += `<div class="bnd-consent"> secC += `<div class="bnd-consent">
<i class="bnd-consent-ico ${ico}"></i> <i class="bnd-consent-ico ${ico}"></i>
<span style="font-size:.82rem">${c.formulario_nombre}</span> <span style="font-size:.82rem">${c.formulario_nombre}</span>
${c.firmado_at ? `<span style="font-size:.72rem;color:#94a3b8">${_hora(c.firmado_at)}</span>` : ''} ${c.firmado_at?`<span style="font-size:.71rem;color:#94a3b8">${_hora(c.firmado_at)}</span>`:''}
${link} ${link}
</div>`; </div>`;
}); });
@@ -572,50 +503,48 @@ function _renderDetalle(d) {
secC += `</div>`; secC += `</div>`;
// Comentarios // Comentarios
let secCom = `<div class="bnd-sec" id="bnd-com-sec"><div class="bnd-sec-title"><i class="fas fa-comments"></i> Comentarios</div>`; let secCom = `<div class="bnd-sec"><div class="bnd-sec-title"><i class="fas fa-comments"></i> Comentarios</div>
secCom += `<div class="bnd-com-list" id="bnd-com-list">`; <div class="bnd-com-list" id="bnd-com-list">`;
if (!d.comentarios.length) { if (!d.comentarios?.length) {
secCom += `<span style="color:#94a3b8;font-size:.82rem">Sin comentarios</span>`; secCom += `<span style="color:#94a3b8;font-size:.82rem">Sin comentarios</span>`;
} else { } else {
d.comentarios.forEach(c => { d.comentarios.forEach(c => {
const fecha = new Date((c.creado_at || '').replace(' ','T')); const tStr = new Date((c.creado_at||'').replace(' ','T'))
const timeStr = fecha.toLocaleTimeString('es-CO', { hour:'2-digit', minute:'2-digit' }); .toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'});
secCom += `<div class="bnd-com"> secCom += `<div class="bnd-com">
<div class="bnd-com-header"> <div class="bnd-com-header">
<span class="bnd-com-tipo ${c.tipo}">${c.tipo}</span> <span class="bnd-com-tipo ${c.tipo}">${c.tipo}</span>
<span class="bnd-com-user">${c.usuario_nombre || ''}</span> <span class="bnd-com-user">${c.usuario_nombre||''}</span>
<span class="bnd-com-date">${timeStr}</span> <span class="bnd-com-date">${tStr}</span>
</div> </div>
<div class="bnd-com-text">${c.comentario}</div> <div class="bnd-com-text">${c.comentario}</div>
</div>`; </div>`;
}); });
} }
secCom += `</div>`; secCom += `</div>
<div class="bnd-add-com" style="margin-top:10px">
// Agregar comentario <textarea id="bnd-new-com" placeholder="Agregar comentario de toma de muestras..."></textarea>
secCom += `<div class="bnd-add-com" style="margin-top:12px">
<textarea id="bnd-new-com" placeholder="Agregar comentario de toma de muestras..." rows="2"></textarea>
<button onclick="_bndEnviarCom(${t.id})"><i class="fas fa-paper-plane"></i> Enviar</button> <button onclick="_bndEnviarCom(${t.id})"><i class="fas fa-paper-plane"></i> Enviar</button>
</div></div>`; </div></div>`;
document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secC + secCom; document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secC + secCom;
} }
// ── Cerrar turno ───────────────────────────────────────────── // ── Marcar visto ─────────────────────────────────────────────
async function _bndCerrar(turnoId) { async function _bndMarcarVisto(turnoId) {
const res = await fetch('modules/turnero/api/cambiar_estado.php', { const res = await fetch(VISTO_API, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type':'application/json'},
body: JSON.stringify({ turno_id: turnoId, nuevo_estado: 'finalizado' }) body: JSON.stringify({turno_id: turnoId})
}).then(r => r.json()).catch(() => null); }).then(r=>r.json()).catch(()=>null);
if (!res?.ok) { _toast('Error al cerrar turno'); return; } if (!res?.ok) { _toast('Error al marcar como visto'); return; }
_toast('Turno cerrado'); _toast('Marcado como visto');
await _bndRefrescarLista(); await _bndRefrescarLista();
_bndVerDetalle(turnoId); _bndVerDetalle(turnoId);
} }
// ── Enviar comentario ──────────────────────────────────────── // ── Comentario ────────────────────────────────────────────────
async function _bndEnviarCom(turnoId) { async function _bndEnviarCom(turnoId) {
const ta = document.getElementById('bnd-new-com'); const ta = document.getElementById('bnd-new-com');
const txt = ta.value.trim(); const txt = ta.value.trim();
@@ -625,21 +554,20 @@ async function _bndEnviarCom(turnoId) {
const res = await fetch(COM_API, { const res = await fetch(COM_API, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type':'application/json'},
body: JSON.stringify({ turno_id: turnoId, comentario: txt, tipo: 'muestras' }) body: JSON.stringify({turno_id: turnoId, comentario: txt, tipo: 'muestras'})
}).then(r => r.json()).catch(() => null); }).then(r=>r.json()).catch(()=>null);
btn.disabled = false; btn.disabled = false;
if (!res?.ok) { _toast('Error al guardar comentario'); return; } if (!res?.ok) { _toast('Error al guardar comentario'); return; }
ta.value = ''; ta.value = '';
_toast('Comentario guardado'); _toast('Comentario guardado');
// Recargar sólo la sección de comentarios
_bndVerDetalle(turnoId); _bndVerDetalle(turnoId);
} }
// ── Auto-refresh cada 30s ───────────────────────────────────── // ── Init ──────────────────────────────────────────────────────
_bndRefrescarLista(); _bndRefrescarLista();
_bndInterval = setInterval(_bndRefrescarLista, 30000); setInterval(_bndRefrescarLista, 30000);
</script> </script>
<?php Layout::close(); ?> <?php Layout::close(); ?>