Una curva de glucosa dura dos horas, pero de esas el puesto solo trabaja unos minutos al principio y otros al final: el resto el paciente está esperando el examen. El promedio contaba esas horas como tiempo de atención. Ahora, cuando el turno tiene un formulario de tomas prolongadas, el servicio se corta en la primera toma (toma_inicio_at). Sin protocolo, nada cambia. El efecto es grande y explica por qué los números no cuadraban: promedio general 21,9 min → 7,4 min Omaira Limas 50,9 min → 22,6 min Luisa Gil 32,0 min → 7,9 min Yesica Morales 23,5 min → 6,5 min Lo importante no es que el número baje sino que era injusto: quien atendía protocolos prolongados aparecía como la más lenta cuando estaba haciendo el mismo trabajo que las demás. El promedio corregido coincide con el de los turnos que nunca tuvieron protocolo (8,2 min), que es la comprobación de que el cálculo ahora mide lo que dice medir. Se aplica a los tres promedios: general, por puesto y por profesional. El detalle de cada turno sigue mostrando el tiempo real transcurrido. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
317 lines
13 KiB
PHP
317 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* modules/turnero/api/get_dashboard.php
|
|
* GET ?fecha=YYYY-MM-DD (por defecto = hoy)
|
|
*
|
|
* Devuelve métricas del día para el dashboard del turnero:
|
|
* - 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';
|
|
|
|
requireTurnero();
|
|
requireMethod('GET');
|
|
|
|
$fecha = trim($_GET['fecha'] ?? '');
|
|
if ($fecha === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
|
|
$fecha = date('Y-m-d');
|
|
}
|
|
|
|
$pdo = db();
|
|
|
|
// ── 1. Sesión del día ─────────────────────────────────────────
|
|
$stmtSesion = $pdo->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,
|
|
]);
|