diff --git a/api/lab/get_metricas.php b/api/lab/get_metricas.php
new file mode 100644
index 0000000..83f7e19
--- /dev/null
+++ b/api/lab/get_metricas.php
@@ -0,0 +1,187 @@
+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]
+ );
+
+ 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,
+ ]);
+
+} catch (Exception $e) {
+ error_log('[get_metricas] ' . $e->getMessage());
+ jsonError($e->getMessage(), 500);
+}
diff --git a/classes/lab/Enfermera.php b/classes/lab/Enfermera.php
index 31204cf..58ad676 100644
--- a/classes/lab/Enfermera.php
+++ b/classes/lab/Enfermera.php
@@ -115,6 +115,8 @@ class Enfermera {
d.pago_modo,
d.pago_monto,
d.pago_notas,
+ d.hora_llegada,
+ d.hora_salida,
p.id AS paciente_id,
p.nombre_completo AS paciente_nombre,
p.telefono AS paciente_telefono,
diff --git a/enfermero_portal.php b/enfermero_portal.php
index 961d9a6..dd9e7c0 100644
--- a/enfermero_portal.php
+++ b/enfermero_portal.php
@@ -531,6 +531,20 @@ const portal = {
${cobroHtml}
+
+ ${(item.hora_llegada || item.hora_salida) ? `
+