fetchAll( "SELECT DATE(fecha_programada) AS dia, COUNT(*) AS total, SUM(estado = 'completado') AS completados, SUM(estado = 'cancelado') AS cancelados, SUM(estado = 'reprogramado') AS reprogramados FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? GROUP BY DATE(fecha_programada) ORDER BY dia ASC", [$desde, $hasta] ); // ── 2. KPIs de tasa de éxito ──────────────────────────────────────────── $kpi = $db->fetch( "SELECT COUNT(*) AS total, SUM(estado = 'completado') AS completados, SUM(estado = 'cancelado') AS cancelados, SUM(estado = 'reprogramado') AS reprogramados, SUM(estado NOT IN ('completado','cancelado','reprogramado')) AS en_curso FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ?", [$desde, $hasta] ) ?: []; // ── 3. Tiempo promedio en domicilio ────────────────────────────────────── $tiempos = $db->fetch( "SELECT ROUND(AVG(TIMESTAMPDIFF(MINUTE, hora_llegada, hora_salida)), 1) AS promedio_min, MIN(TIMESTAMPDIFF(MINUTE, hora_llegada, hora_salida)) AS min_min, MAX(TIMESTAMPDIFF(MINUTE, hora_llegada, hora_salida)) AS max_min, COUNT(*) AS con_tiempos FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? AND hora_llegada IS NOT NULL AND hora_salida IS NOT NULL AND hora_salida > hora_llegada", [$desde, $hasta] ) ?: []; // ── 4. Tiempo promedio desde agendamiento hasta realización ───────────── $tiempoAgen = $db->fetch( "SELECT ROUND(AVG(DATEDIFF(fecha_programada, DATE(created_at))), 1) AS dias_promedio, MIN(DATEDIFF(fecha_programada, DATE(created_at))) AS dias_min, MAX(DATEDIFF(fecha_programada, DATE(created_at))) AS dias_max FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? AND estado = 'completado'", [$desde, $hasta] ) ?: []; // ── 5. Ranking de enfermeras ──────────────────────────────────────────── $rankingEnf = $db->fetchAll( "SELECT e.id, e.nombre_completo, COUNT(a.id) AS asignados, SUM(d.estado = 'completado') AS completados, SUM(d.estado = 'cancelado') AS cancelados, ROUND(SUM(d.estado='completado') / NULLIF(COUNT(a.id),0) * 100, 1) AS tasa_pct FROM lab_asignaciones a JOIN lab_enfermeras e ON e.id = a.enfermera_id JOIN lab_domicilios d ON d.id = a.domicilio_id WHERE d.fecha_programada BETWEEN ? AND ? GROUP BY e.id, e.nombre_completo ORDER BY completados DESC, asignados DESC", [$desde, $hasta] ); // ── 6. Financiero ──────────────────────────────────────────────────────── $financiero = $db->fetch( "SELECT SUM(COALESCE(valor_domicilio,0) + COALESCE(valor_copago,0)) AS total_bruto, SUM(CASE WHEN pago_estado='pagado' THEN COALESCE(valor_domicilio,0)+COALESCE(valor_copago,0) ELSE 0 END) AS cobrado, SUM(CASE WHEN pago_estado='pendiente' THEN COALESCE(valor_domicilio,0)+COALESCE(valor_copago,0) ELSE 0 END) AS pendiente, SUM(CASE WHEN pago_estado='exento' THEN COALESCE(valor_domicilio,0)+COALESCE(valor_copago,0) ELSE 0 END) AS exento, COUNT(CASE WHEN pago_estado='pagado' THEN 1 END) AS n_pagados, COUNT(CASE WHEN pago_estado='pendiente' THEN 1 END) AS n_pendientes, COUNT(CASE WHEN pago_estado='exento' THEN 1 END) AS n_exentos FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ?", [$desde, $hasta] ) ?: []; // ── 7. Por modo de pago ───────────────────────────────────────────────── $porModoPago = $db->fetchAll( "SELECT COALESCE(pago_modo, 'no_especificado') AS modo, COUNT(*) AS n, SUM(COALESCE(valor_domicilio,0)+COALESCE(valor_copago,0)) AS total FROM lab_domicilios WHERE pago_estado = 'pagado' AND fecha_programada BETWEEN ? AND ? GROUP BY pago_modo ORDER BY n DESC", [$desde, $hasta] ); // ── 8. Top zonas (barrio / ciudad) ────────────────────────────────────── $topZonas = $db->fetchAll( "SELECT COALESCE(NULLIF(TRIM(barrio),''), NULLIF(TRIM(ciudad),''), 'Sin datos') AS zona, COUNT(*) AS n FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? GROUP BY zona ORDER BY n DESC LIMIT 10", [$desde, $hasta] ); // ── 9. Domicilios por tipo de cliente ──────────────────────────────────── $porTipoCliente = $db->fetchAll( "SELECT COALESCE(tipo_cliente,'particular') AS tipo, COALESCE(seguro_nombre,'') AS seguro, COUNT(*) AS n FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? GROUP BY tipo, seguro ORDER BY n DESC", [$desde, $hasta] ); // ── 10. Top exámenes solicitados (simple word-frequency) ───────────────── // Traemos todos los valores y hacemos el conteo en PHP para no depender de // funciones de texto avanzadas de MySQL que podrían variar según servidor. $examRows = $db->fetchAll( "SELECT examenes_solicitados FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? AND examenes_solicitados IS NOT NULL AND examenes_solicitados != ''", [$desde, $hasta] ); $examConteo = []; foreach ($examRows as $row) { // Dividir por coma, punto y coma o salto de línea $items = preg_split('/[,;\n]+/', $row['examenes_solicitados'] ?? ''); foreach ($items as $item) { $item = trim($item); if (strlen($item) > 2) { $k = mb_strtolower($item); $examConteo[$k] = ($examConteo[$k] ?? 0) + 1; } } } arsort($examConteo); $topExamenes = array_slice( array_map(fn($k, $v) => ['examen' => $k, 'n' => $v], array_keys($examConteo), $examConteo), 0, 10 ); // ── 11. Horas del día con más agendamientos ───────────────────────────── $porHora = $db->fetchAll( "SELECT HOUR(hora_programada) AS hora, COUNT(*) AS n FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? AND hora_programada IS NOT NULL GROUP BY HOUR(hora_programada) ORDER BY hora ASC", [$desde, $hasta] ); // ── 12. Tiempo de traslado: desde 'en_camino' hasta 'en_domicilio' ──────── // Fuente: columnas hora_en_camino y hora_llegada de lab_domicilios. // hora_en_camino se guarda cuando el enfermero marca 'En camino'. // hora_llegada se guarda cuando el enfermero marca 'En domicilio'. // Se excluyen filas donde la llegada sea anterior a la salida (datos corruptos). $traslado = $db->fetch( "SELECT ROUND(AVG(TIMESTAMPDIFF(MINUTE, CONCAT(fecha_programada,' ',hora_en_camino), CONCAT(fecha_programada,' ',hora_llegada))), 1) AS promedio_min, MIN(TIMESTAMPDIFF(MINUTE, CONCAT(fecha_programada,' ',hora_en_camino), CONCAT(fecha_programada,' ',hora_llegada))) AS min_min, MAX(TIMESTAMPDIFF(MINUTE, CONCAT(fecha_programada,' ',hora_en_camino), CONCAT(fecha_programada,' ',hora_llegada))) AS max_min, COUNT(*) AS con_datos, SUM(TIMESTAMPDIFF(MINUTE, CONCAT(fecha_programada,' ',hora_en_camino), CONCAT(fecha_programada,' ',hora_llegada)) <= 15) AS dentro_15min, SUM(TIMESTAMPDIFF(MINUTE, CONCAT(fecha_programada,' ',hora_en_camino), CONCAT(fecha_programada,' ',hora_llegada)) <= 30) AS dentro_30min FROM lab_domicilios WHERE fecha_programada BETWEEN ? AND ? AND hora_en_camino IS NOT NULL AND hora_llegada IS NOT NULL AND hora_llegada > hora_en_camino", [$desde, $hasta] ) ?: []; // ── 13. Tiempo de traslado por enfermera (top 10 más rápidas) ───────────── $trasladoPorEnfermera = $db->fetchAll( "SELECT e.nombre_completo, ROUND(AVG(TIMESTAMPDIFF(MINUTE, CONCAT(d.fecha_programada,' ',d.hora_en_camino), CONCAT(d.fecha_programada,' ',d.hora_llegada))), 1) AS promedio_min, COUNT(*) AS viajes FROM lab_domicilios d JOIN lab_asignaciones a ON a.domicilio_id = d.id AND a.estado NOT IN ('liberada') JOIN lab_enfermeras e ON e.id = a.enfermera_id WHERE d.fecha_programada BETWEEN ? AND ? AND d.hora_en_camino IS NOT NULL AND d.hora_llegada IS NOT NULL AND d.hora_llegada > d.hora_en_camino GROUP BY e.id, e.nombre_completo ORDER BY promedio_min ASC LIMIT 10", [$desde, $hasta] ); jsonOk([ 'por_dia' => $porDia, 'kpi' => $kpi, 'tiempos' => $tiempos, 'tiempo_agendamiento' => $tiempoAgen, 'ranking_enfermeras' => $rankingEnf, 'financiero' => $financiero, 'por_modo_pago' => $porModoPago, 'top_zonas' => $topZonas, 'por_tipo_cliente' => $porTipoCliente, 'top_examenes' => $topExamenes, 'por_hora' => $porHora, 'traslado' => $traslado, 'traslado_por_enfermera' => $trasladoPorEnfermera, ]); } catch (Exception $e) { error_log('[get_metricas] ' . $e->getMessage()); jsonError($e->getMessage(), 500); }