prepare( 'SELECT id, inicio_at, fin_at FROM turnero_sesiones WHERE fecha = ? LIMIT 1' ); $stmtSesion->execute([$fecha]); $sesion = $stmtSesion->fetch(PDO::FETCH_ASSOC); if (!$sesion) { jsonOk([ 'fecha' => $fecha, 'sesion' => null, 'resumen' => [ 'total' => 0, 'atendidos' => 0, 'en_espera' => 0, 'ausentes' => 0, 'cancelados' => 0, 'tiempo_espera_promedio_min' => null, 'tiempo_servicio_promedio_min' => null, ], 'por_prioridad' => [], 'por_lugar' => [], 'por_recepcionista' => [], 'por_bacteriologo' => [], 'consent_stats' => [], 'turnos' => [], ]); } $sesionId = (int)$sesion['id']; // ── 2. Resumen global ───────────────────────────────────────── $stmtRes = $pdo->prepare( "SELECT COUNT(*) AS total, SUM(t.estado IN ('finalizado','en_servicio')) AS atendidos, SUM(t.estado IN ('espera','en_recepcion','en_espera_lugar')) AS en_espera, SUM(t.estado = 'ausente') AS ausentes, SUM(t.estado = 'cancelado') AS cancelados, ROUND( AVG( CASE WHEN t.inicio_recepcion_at IS NOT NULL AND t.creado_at IS NOT NULL THEN TIMESTAMPDIFF(SECOND, t.creado_at, t.inicio_recepcion_at) / 60.0 END ), 1 ) AS tiempo_espera_promedio_min, -- En los protocolos prolongados la atención se corta en la PRIMERA toma: -- lo que sigue son horas de espera del examen (una curva de glucosa son -- 2 horas), no tiempo de trabajo. Contarlas triplicaba el promedio —21,9 -- min contra 7,4— y hacía ver los puestos mucho más lentos de lo que son. ROUND( AVG( CASE WHEN t.inicio_lugar_at IS NOT NULL AND COALESCE(prot.toma_inicio_at, t.fin_lugar_at) IS NOT NULL THEN TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(prot.toma_inicio_at, t.fin_lugar_at)) / 60.0 END ), 1 ) AS tiempo_servicio_promedio_min, ROUND( AVG( CASE WHEN t.creado_at IS NOT NULL AND COALESCE(prot.toma_inicio_at, t.fin_lugar_at) IS NOT NULL THEN TIMESTAMPDIFF(SECOND, t.creado_at, COALESCE(prot.toma_inicio_at, t.fin_lugar_at)) / 60.0 END ), 1 ) AS tiempo_total_promedio_min FROM turnero_turnos t -- Un turno tiene a lo sumo un protocolo prolongado, así que el join no duplica LEFT JOIN ( SELECT tc.turno_id, MIN(tc.toma_inicio_at) AS toma_inicio_at FROM turnero_consentimientos tc JOIN lab_formularios f ON f.id = tc.formulario_id AND f.es_toma_progresiva = 1 GROUP BY tc.turno_id ) prot ON prot.turno_id = t.id WHERE t.sesion_id = ?" ); $stmtRes->execute([$sesionId]); $resumen = $stmtRes->fetch(PDO::FETCH_ASSOC); // ── 3. Desglose por prioridad ───────────────────────────────── $stmtPri = $pdo->prepare( "SELECT p.codigo, p.nombre, p.color, COUNT(t.id) AS total, SUM(t.estado IN ('finalizado','en_servicio')) AS atendidos, SUM(t.estado IN ('espera','en_recepcion','en_espera_lugar')) AS pendientes, SUM(t.estado = 'ausente') AS ausentes FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id WHERE t.sesion_id = ? GROUP BY p.id ORDER BY p.orden_peso ASC" ); $stmtPri->execute([$sesionId]); $porPrioridad = $stmtPri->fetchAll(PDO::FETCH_ASSOC); // ── 4. Desglose por lugar ───────────────────────────────────── $stmtLug = $pdo->prepare( "SELECT l.nombre AS lugar, COUNT(t.id) AS total, SUM(t.estado IN ('finalizado','en_servicio')) AS atendidos, SUM(t.estado IN ('en_espera_lugar')) AS en_espera, ROUND(AVG( CASE WHEN t.inicio_lugar_at IS NOT NULL AND COALESCE(prot.toma_inicio_at, t.fin_lugar_at) IS NOT NULL THEN TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(prot.toma_inicio_at, t.fin_lugar_at)) / 60.0 END ), 1) AS tiempo_servicio_promedio_min FROM turnero_turnos t JOIN turnero_lugares l ON l.id = t.lugar_destino_id LEFT JOIN ( SELECT tc.turno_id, MIN(tc.toma_inicio_at) AS toma_inicio_at FROM turnero_consentimientos tc JOIN lab_formularios f ON f.id = tc.formulario_id AND f.es_toma_progresiva = 1 GROUP BY tc.turno_id ) prot ON prot.turno_id = t.id WHERE t.sesion_id = ? GROUP BY l.id ORDER BY l.sort_order ASC, l.nombre ASC" ); $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.inicio_lugar_at IS NOT NULL AND COALESCE(prot.toma_inicio_at, t.fin_lugar_at) IS NOT NULL THEN TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(prot.toma_inicio_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 LEFT JOIN ( SELECT tc.turno_id, MIN(tc.toma_inicio_at) AS toma_inicio_at FROM turnero_consentimientos tc JOIN lab_formularios f ON f.id = tc.formulario_id AND f.es_toma_progresiva = 1 GROUP BY tc.turno_id ) prot ON prot.turno_id = t.id 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, t.paciente_nombre, t.paciente_cel, t.creado_at, t.inicio_recepcion_at, t.fin_recepcion_at, t.inicio_lugar_at, t.fin_lugar_at, p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre, p.color AS prioridad_color, l.nombre AS lugar_nombre, s.nombre_completo AS paciente_bd, s.numero_documento AS paciente_doc, s.tipo_documento AS paciente_tipo_doc, rd.nombre AS recepcion_desk_nombre, ur.full_name AS atendido_recepcion_nombre, ul.full_name AS atendido_lugar_nombre, ts.observaciones AS notas, ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, COALESCE(t.inicio_recepcion_at, NOW())) / 60.0, 1) AS espera_min, ROUND(TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(t.fin_lugar_at, NOW())) / 60.0, 1) AS servicio_min, ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, t.fin_lugar_at) / 60.0, 1) AS total_min FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id LEFT JOIN turnero_lugares l ON l.id = t.lugar_destino_id LEFT JOIN turnero_lugares rd ON rd.id = t.recepcion_desk_id LEFT JOIN admin_users ur ON ur.id = t.atendido_recepcion_por LEFT JOIN admin_users ul ON ul.id = t.atendido_lugar_por LEFT JOIN turnero_solicitudes ts ON ts.turno_id = t.id LEFT JOIN lab_pacientes s ON s.id = ts.paciente_id WHERE t.sesion_id = ? ORDER BY t.numero ASC" ); $stmtTurnos->execute([$sesionId]); $turnos = $stmtTurnos->fetchAll(PDO::FETCH_ASSOC); // 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 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($turnoIds); foreach ($stmtEx->fetchAll(PDO::FETCH_ASSOC) as $row) { $examenesPorTurno[(int)$row['turno_id']][] = ['nombre' => $row['nombre']]; } $stmtCom = $pdo->prepare( "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($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); // ── 6. Consentimientos del día ──────────────────────────────── $stmtcons = $pdo->prepare( "SELECT tc.estado, COUNT(*) AS cnt FROM turnero_consentimientos tc JOIN turnero_turnos t ON t.id = tc.turno_id WHERE t.sesion_id = ? GROUP BY tc.estado" ); $stmtcons->execute([$sesionId]); $consentStats = []; foreach ($stmtcons->fetchAll(PDO::FETCH_ASSOC) as $r) { $consentStats[$r['estado']] = (int)$r['cnt']; } jsonOk([ 'fecha' => $fecha, 'sesion' => [ 'id' => $sesionId, 'inicio' => $sesion['inicio_at'] ? date('H:i', strtotime($sesion['inicio_at'])) : null, 'fin' => $sesion['fin_at'] ? date('H:i', strtotime($sesion['fin_at'])) : null, 'abierta' => $sesion['fin_at'] === null, ], 'resumen' => $resumen, 'por_prioridad' => $porPrioridad, 'por_lugar' => $porLugar, 'por_recepcionista' => $porRecepcionista, 'por_bacteriologo' => $porBacteriologo, 'consent_stats' => $consentStats, 'turnos' => $turnos, ]);