diff --git a/modules/turnero/api/get_dashboard.php b/modules/turnero/api/get_dashboard.php index 148fd1f..a675c17 100644 --- a/modules/turnero/api/get_dashboard.php +++ b/modules/turnero/api/get_dashboard.php @@ -7,6 +7,7 @@ * - resumen global (total, atendidos, pendientes, ausentes, tiempo promedio) * - desglose por prioridad * - desglose por lugar + * - desglose por recepcionista y por bacteriólogo (quién atendió a cuántos) * - lista de turnos del día (para tabla detalle) */ require_once __DIR__ . '/_helpers.php'; @@ -41,10 +42,12 @@ if (!$sesion) { 'tiempo_espera_promedio_min' => null, 'tiempo_servicio_promedio_min' => null, ], - 'por_prioridad' => [], - 'por_lugar' => [], - 'consent_stats' => [], - 'turnos' => [], + 'por_prioridad' => [], + 'por_lugar' => [], + 'por_recepcionista' => [], + 'por_bacteriologo' => [], + 'consent_stats' => [], + 'turnos' => [], ]); } @@ -125,6 +128,55 @@ $stmtLug = $pdo->prepare( $stmtLug->execute([$sesionId]); $porLugar = $stmtLug->fetchAll(PDO::FETCH_ASSOC); +// ── 4b. Desglose por recepcionista ──────────────────────────── +// Mismo molde que el ranking de enfermeras de lab_domicilios +// (api/lab/get_metricas.php). atendido_recepcion_por guarda al PRIMER +// recepcionista que tomó el turno (COALESCE en cambiar_estado.php). +$stmtRec = $pdo->prepare( + "SELECT u.id, + COALESCE(NULLIF(u.full_name, ''), u.username) AS nombre, + COUNT(t.id) AS total, + SUM(t.estado IN ('finalizado','en_servicio','en_espera_lugar')) AS atendidos, + SUM(t.estado = 'ausente') AS ausentes, + SUM(t.estado = 'cancelado') AS cancelados, + ROUND(AVG( + CASE + WHEN t.fin_recepcion_at IS NOT NULL AND t.inicio_recepcion_at IS NOT NULL + THEN TIMESTAMPDIFF(SECOND, t.inicio_recepcion_at, t.fin_recepcion_at) / 60.0 + END + ), 1) AS tiempo_promedio_min + FROM turnero_turnos t + JOIN admin_users u ON u.id = t.atendido_recepcion_por + WHERE t.sesion_id = ? + GROUP BY u.id, u.full_name, u.username + ORDER BY atendidos DESC, total DESC" +); +$stmtRec->execute([$sesionId]); +$porRecepcionista = $stmtRec->fetchAll(PDO::FETCH_ASSOC); + +// ── 4c. Desglose por bacteriólogo (quien atendió en el lugar) ── +$stmtBac = $pdo->prepare( + "SELECT u.id, + COALESCE(NULLIF(u.full_name, ''), u.username) AS nombre, + COUNT(t.id) AS total, + SUM(t.estado = 'finalizado') AS finalizados, + SUM(t.estado = 'en_servicio') AS en_servicio, + SUM(t.estado = 'ausente') AS ausentes, + ROUND(AVG( + CASE + WHEN t.fin_lugar_at IS NOT NULL AND t.inicio_lugar_at IS NOT NULL + THEN TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, t.fin_lugar_at) / 60.0 + END + ), 1) AS tiempo_promedio_min + FROM turnero_turnos t + JOIN admin_users u ON u.id = t.atendido_lugar_por + WHERE t.sesion_id = ? + GROUP BY u.id, u.full_name, u.username + ORDER BY finalizados DESC, total DESC" +); +$stmtBac->execute([$sesionId]); +$porBacteriologo = $stmtBac->fetchAll(PDO::FETCH_ASSOC); + // ── 5. Lista de turnos del día ──────────────────────────────── $stmtTurnos = $pdo->prepare( "SELECT t.id, t.codigo, t.numero, t.estado, @@ -157,22 +209,47 @@ $stmtTurnos = $pdo->prepare( $stmtTurnos->execute([$sesionId]); $turnos = $stmtTurnos->fetchAll(PDO::FETCH_ASSOC); -// Enriquecer con exámenes y comentarios -foreach ($turnos as &$turno) { +// Enriquecer con exámenes y comentarios — en lote. +// Antes eran 2 consultas por turno dentro del foreach (N+1): con 80 turnos en +// el día eran 160 consultas evitables. Mismo patrón que get_historial.php. +$turnoIds = array_column($turnos, 'id'); +$examenesPorTurno = []; +$comentariosPorTurno = []; + +if ($turnoIds) { + $ph = implode(',', array_fill(0, count($turnoIds), '?')); + $stmtEx = $pdo->prepare( - "SELECT DISTINCT et.nombre FROM turnero_examen_items tei - JOIN exam_tipos et ON et.id = tei.exam_tipo_id - WHERE tei.solicitud_id = (SELECT id FROM turnero_solicitudes WHERE turno_id = ? LIMIT 1)" + "SELECT DISTINCT ts.turno_id, et.nombre + FROM turnero_solicitudes ts + JOIN turnero_examen_items tei ON tei.solicitud_id = ts.id + JOIN exam_tipos et ON et.id = tei.exam_tipo_id + WHERE ts.turno_id IN ($ph) + ORDER BY et.nombre ASC" ); - $stmtEx->execute([$turno['id']]); - $turno['examenes'] = $stmtEx->fetchAll(PDO::FETCH_ASSOC); + $stmtEx->execute($turnoIds); + foreach ($stmtEx->fetchAll(PDO::FETCH_ASSOC) as $row) { + $examenesPorTurno[(int)$row['turno_id']][] = ['nombre' => $row['nombre']]; + } $stmtCom = $pdo->prepare( - "SELECT usuario_nombre, comentario, tipo, creado_at - FROM turnero_comentarios WHERE turno_id = ? ORDER BY creado_at ASC" + "SELECT turno_id, usuario_nombre, comentario, tipo, creado_at + FROM turnero_comentarios + WHERE turno_id IN ($ph) + ORDER BY turno_id, creado_at ASC" ); - $stmtCom->execute([$turno['id']]); - $turno['comentarios'] = $stmtCom->fetchAll(PDO::FETCH_ASSOC); + $stmtCom->execute($turnoIds); + foreach ($stmtCom->fetchAll(PDO::FETCH_ASSOC) as $row) { + $tid = (int)$row['turno_id']; + unset($row['turno_id']); + $comentariosPorTurno[$tid][] = $row; + } +} + +foreach ($turnos as &$turno) { + $tid = (int)$turno['id']; + $turno['examenes'] = $examenesPorTurno[$tid] ?? []; + $turno['comentarios'] = $comentariosPorTurno[$tid] ?? []; } unset($turno); @@ -201,6 +278,8 @@ jsonOk([ 'resumen' => $resumen, 'por_prioridad' => $porPrioridad, 'por_lugar' => $porLugar, + 'por_recepcionista' => $porRecepcionista, + 'por_bacteriologo' => $porBacteriologo, 'consent_stats' => $consentStats, 'turnos' => $turnos, ]); diff --git a/modules/turnero/views/dashboard.php b/modules/turnero/views/dashboard.php index 2e15bb5..6911d8d 100644 --- a/modules/turnero/views/dashboard.php +++ b/modules/turnero/views/dashboard.php @@ -287,6 +287,18 @@ try {
+ + + + + +
@@ -445,7 +457,8 @@ function mostrarError(msg) { // ── Render ─────────────────────────────────────────────────── function renderDashboard(json) { - const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos } = json; + const { sesion, resumen, por_prioridad, por_lugar, consent_stats = {}, turnos, + por_recepcionista = [], por_bacteriologo = [] } = json; // ── Sesión info ──────────────────────────────────────── const si = document.getElementById('sesionInfo'); @@ -536,11 +549,41 @@ function renderDashboard(json) { secLug.style.display = 'none'; } + // ── Tarjetas por persona ─────────────────────────────── + renderPersonas('sectionRecepcionista', 'recepcionistaGrid', por_recepcionista, [ + ['Turnos', p => p.total], + ['Atendidos', p => p.atendidos], + ['Ausentes', p => p.ausentes], + ['T. recepción prom.', p => p.tiempo_promedio_min !== null ? p.tiempo_promedio_min + ' min' : '—'], + ]); + + renderPersonas('sectionBacteriologo', 'bacteriologoGrid', por_bacteriologo, [ + ['Turnos', p => p.total], + ['Finalizados', p => p.finalizados], + ['En servicio', p => p.en_servicio], + ['T. servicio prom.', p => p.tiempo_promedio_min !== null ? p.tiempo_promedio_min + ' min' : '—'], + ]); + // ── Tabla de turnos ──────────────────────────────────── _turnos = turnos; renderTabla(turnos); } +// Tarjetas "quién atendió a cuántos". Reutiliza el grid de lugares para que +// las tres secciones se vean iguales. +function renderPersonas(sectionId, gridId, filas, columnas) { + const sec = document.getElementById(sectionId); + if (!filas || !filas.length) { sec.style.display = 'none'; return; } + sec.style.display = ''; + document.getElementById(gridId).innerHTML = filas.map(p => ` +
+
${escHtml(p.nombre || '—')}
+ ${columnas.map(([lbl, val]) => + `
${lbl}${escHtml(String(val(p) ?? '—'))}
` + ).join('')} +
`).join(''); +} + function kpiCard(val, lbl, sub) { return `
${val !== null && val !== undefined ? escHtml(String(val)) : '—'}