cambios importates
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/**
|
||||
* GET /api/kpi_actividad.php
|
||||
*
|
||||
* Devuelve métricas de actividad de pacientes por rango de fechas:
|
||||
* - Por día: usuarios únicos que escriben y agendamientos creados.
|
||||
* - Por día de la semana: agregado del rango seleccionado.
|
||||
*
|
||||
* Parámetros GET:
|
||||
* desde (YYYY-MM-DD) — fecha inicio, default hoy-29 días
|
||||
* hasta (YYYY-MM-DD) — fecha fin, default hoy
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
error_reporting(E_ERROR | E_PARSE);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
requireAuthentication();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
// ── Rango de fechas ──────────────────────────────────────
|
||||
$hoy = date('Y-m-d');
|
||||
$desde = $_GET['desde'] ?? date('Y-m-d', strtotime('-29 days'));
|
||||
$hasta = $_GET['hasta'] ?? $hoy;
|
||||
|
||||
// Sanitizar: solo fechas válidas
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $desde)) $desde = date('Y-m-d', strtotime('-29 days'));
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $hasta)) $hasta = $hoy;
|
||||
// No permitir rangos invertidos
|
||||
if ($desde > $hasta) [$desde, $hasta] = [$hasta, $desde];
|
||||
|
||||
// ── Mensajes entrantes por día (usuarios únicos) ─────────
|
||||
$msgRows = $db->fetchAll("
|
||||
SELECT
|
||||
DATE(created_at) AS fecha,
|
||||
COUNT(DISTINCT user_id) AS usuarios,
|
||||
COUNT(*) AS total_mensajes
|
||||
FROM conversations
|
||||
WHERE direction = 'incoming'
|
||||
AND DATE(created_at) BETWEEN ? AND ?
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY fecha ASC
|
||||
", [$desde, $hasta]);
|
||||
|
||||
// ── Agendamientos (turnos creados) por día ───────────────
|
||||
$turnosRows = $db->fetchAll("
|
||||
SELECT
|
||||
DATE(creado_at) AS fecha,
|
||||
COUNT(*) AS agendamientos
|
||||
FROM turnero_turnos
|
||||
WHERE DATE(creado_at) BETWEEN ? AND ?
|
||||
GROUP BY DATE(creado_at)
|
||||
ORDER BY fecha ASC
|
||||
", [$desde, $hasta]);
|
||||
|
||||
// ── Construir mapa fecha → datos combinados ──────────────
|
||||
$dias = [];
|
||||
|
||||
// Generar todos los días del rango para que no haya huecos
|
||||
$cur = new DateTime($desde);
|
||||
$fin = new DateTime($hasta);
|
||||
while ($cur <= $fin) {
|
||||
$f = $cur->format('Y-m-d');
|
||||
$dias[$f] = ['fecha' => $f, 'usuarios' => 0, 'total_mensajes' => 0, 'agendamientos' => 0];
|
||||
$cur->modify('+1 day');
|
||||
}
|
||||
|
||||
foreach ($msgRows as $r) {
|
||||
if (isset($dias[$r['fecha']])) {
|
||||
$dias[$r['fecha']]['usuarios'] = (int)$r['usuarios'];
|
||||
$dias[$r['fecha']]['total_mensajes'] = (int)$r['total_mensajes'];
|
||||
}
|
||||
}
|
||||
foreach ($turnosRows as $r) {
|
||||
if (isset($dias[$r['fecha']])) {
|
||||
$dias[$r['fecha']]['agendamientos'] = (int)$r['agendamientos'];
|
||||
}
|
||||
}
|
||||
|
||||
$porDia = array_values($dias);
|
||||
|
||||
// ── Agrupado por día de la semana (0=Domingo … 6=Sábado) ─
|
||||
$semana = [
|
||||
0 => ['dia' => 'Domingo', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
1 => ['dia' => 'Lunes', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
2 => ['dia' => 'Martes', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
3 => ['dia' => 'Miércoles', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
4 => ['dia' => 'Jueves', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
5 => ['dia' => 'Viernes', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
6 => ['dia' => 'Sábado', 'usuarios' => 0, 'agendamientos' => 0],
|
||||
];
|
||||
|
||||
foreach ($porDia as $d) {
|
||||
$dow = (int)(new DateTime($d['fecha']))->format('w'); // 0=Dom
|
||||
$semana[$dow]['usuarios'] += $d['usuarios'];
|
||||
$semana[$dow]['agendamientos'] += $d['agendamientos'];
|
||||
}
|
||||
|
||||
// ── Totales del rango ────────────────────────────────────
|
||||
$totalUsuarios = array_sum(array_column($porDia, 'usuarios'));
|
||||
$totalMensajes = array_sum(array_column($porDia, 'total_mensajes'));
|
||||
$totalAgendamientos = array_sum(array_column($porDia, 'agendamientos'));
|
||||
|
||||
// Día de semana pico (más usuarios)
|
||||
$picoDow = array_keys($semana, max($semana, fn($a,$b) => $a['usuarios'] <=> $b['usuarios']))[0] ?? null;
|
||||
$picoDow = array_reduce(array_keys($semana), fn($carry, $k) => ($semana[$k]['usuarios'] > $semana[$carry]['usuarios'] ? $k : $carry), 0);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'desde' => $desde,
|
||||
'hasta' => $hasta,
|
||||
'totales' => [
|
||||
'usuarios' => $totalUsuarios,
|
||||
'mensajes' => $totalMensajes,
|
||||
'agendamientos' => $totalAgendamientos,
|
||||
'pico_semana' => $semana[$picoDow]['dia'] ?? '—',
|
||||
],
|
||||
'por_dia' => $porDia,
|
||||
'por_dia_semana' => array_values($semana),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* GET /api/kpi_respuesta_asesor.php
|
||||
*
|
||||
* KPI de tiempos de respuesta de asesores.
|
||||
* Calcula, por cada sesión de atención:
|
||||
* - Tiempo (min) entre "Atender" y la primera respuesta outgoing del asesor.
|
||||
* - Duración total de la sesión (Atender → Finalizar).
|
||||
* Devuelve:
|
||||
* - Promedios globales.
|
||||
* - Desglose por asesor.
|
||||
* - Distribución de tiempos (histograma).
|
||||
* - Serie por día para ver tendencia.
|
||||
*
|
||||
* Parámetros GET:
|
||||
* desde YYYY-MM-DD (default: hoy - 29 días)
|
||||
* hasta YYYY-MM-DD (default: hoy)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
|
||||
error_reporting(E_ERROR | E_PARSE);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
requireAuthentication();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
$hoy = date('Y-m-d');
|
||||
$desde = $_GET['desde'] ?? date('Y-m-d', strtotime('-29 days'));
|
||||
$hasta = $_GET['hasta'] ?? $hoy;
|
||||
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $desde)) $desde = date('Y-m-d', strtotime('-29 days'));
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $hasta)) $hasta = $hoy;
|
||||
if ($desde > $hasta) [$desde, $hasta] = [$hasta, $desde];
|
||||
|
||||
// ── Sesiones de atención en el rango ─────────────────────────────────
|
||||
// Para cada "attend" buscamos:
|
||||
// a) El primer mensaje outgoing del mismo user_id DESPUÉS del attend
|
||||
// y dentro de 4 horas (descartamos bots post-sesión).
|
||||
// b) El primer "finish" del mismo user_id DESPUÉS del attend.
|
||||
$sesiones = $db->fetchAll("
|
||||
SELECT
|
||||
oa.id AS sesion_id,
|
||||
oa.user_id,
|
||||
oa.operator_id,
|
||||
COALESCE(au.full_name, au.username, CONCAT('Asesor #', oa.operator_id)) AS operador,
|
||||
oa.created_at AS attend_at,
|
||||
|
||||
-- Primera respuesta outgoing del asesor
|
||||
(
|
||||
SELECT MIN(c.created_at)
|
||||
FROM conversations c
|
||||
WHERE c.user_id = oa.user_id
|
||||
AND c.direction = 'outgoing'
|
||||
AND c.created_at > oa.created_at
|
||||
AND c.created_at < oa.created_at + INTERVAL 4 HOUR
|
||||
) AS primera_respuesta_at,
|
||||
|
||||
-- Fin de la sesión
|
||||
(
|
||||
SELECT MIN(oa2.created_at)
|
||||
FROM operator_activity oa2
|
||||
WHERE oa2.user_id = oa.user_id
|
||||
AND oa2.action = 'finish'
|
||||
AND oa2.created_at > oa.created_at
|
||||
AND oa2.created_at < oa.created_at + INTERVAL 8 HOUR
|
||||
) AS finish_at
|
||||
|
||||
FROM operator_activity oa
|
||||
LEFT JOIN admin_users au ON au.id = oa.operator_id
|
||||
WHERE oa.action = 'attend'
|
||||
AND DATE(oa.created_at) BETWEEN ? AND ?
|
||||
ORDER BY oa.created_at ASC
|
||||
", [$desde, $hasta]);
|
||||
|
||||
// ── Calcular métricas por sesión ─────────────────────────────────────
|
||||
$totalSesiones = 0;
|
||||
$conRespuesta = 0;
|
||||
$sumaMinRespuesta = 0;
|
||||
$sumaMinDuracion = 0;
|
||||
$conDuracion = 0;
|
||||
$sinRespuesta = 0;
|
||||
|
||||
// Por asesor
|
||||
$asesores = [];
|
||||
|
||||
// Por día (para tendencia)
|
||||
$porDia = [];
|
||||
|
||||
// Histograma: cubos < 1, 1-3, 3-5, 5-10, 10-20, >20 minutos
|
||||
$hist = ['< 1 min' => 0, '1-3 min' => 0, '3-5 min' => 0, '5-10 min' => 0, '10-20 min' => 0, '> 20 min' => 0, 'Sin resp.' => 0];
|
||||
|
||||
foreach ($sesiones as $s) {
|
||||
$totalSesiones++;
|
||||
|
||||
$attendDt = new DateTime($s['attend_at']);
|
||||
$fechaDia = $attendDt->format('Y-m-d');
|
||||
|
||||
if (!isset($porDia[$fechaDia])) {
|
||||
$porDia[$fechaDia] = ['fecha' => $fechaDia, 'sesiones' => 0, 'suma_resp' => 0, 'con_resp' => 0];
|
||||
}
|
||||
$porDia[$fechaDia]['sesiones']++;
|
||||
|
||||
$opId = $s['operator_id'] ?? 0;
|
||||
if (!isset($asesores[$opId])) {
|
||||
$asesores[$opId] = [
|
||||
'operador' => $s['operador'],
|
||||
'sesiones' => 0,
|
||||
'con_resp' => 0,
|
||||
'suma_resp' => 0,
|
||||
'suma_dur' => 0,
|
||||
'con_dur' => 0,
|
||||
];
|
||||
}
|
||||
$asesores[$opId]['sesiones']++;
|
||||
|
||||
// Tiempo de primera respuesta
|
||||
if (!empty($s['primera_respuesta_at'])) {
|
||||
$respDt = new DateTime($s['primera_respuesta_at']);
|
||||
$minResp = round(($respDt->getTimestamp() - $attendDt->getTimestamp()) / 60, 1);
|
||||
if ($minResp < 0) $minResp = 0;
|
||||
|
||||
$conRespuesta++;
|
||||
$sumaMinRespuesta += $minResp;
|
||||
$asesores[$opId]['con_resp']++;
|
||||
$asesores[$opId]['suma_resp'] += $minResp;
|
||||
$porDia[$fechaDia]['suma_resp'] += $minResp;
|
||||
$porDia[$fechaDia]['con_resp']++;
|
||||
|
||||
// Histograma
|
||||
if ($minResp < 1) $hist['< 1 min']++;
|
||||
elseif ($minResp < 3) $hist['1-3 min']++;
|
||||
elseif ($minResp < 5) $hist['3-5 min']++;
|
||||
elseif ($minResp < 10) $hist['5-10 min']++;
|
||||
elseif ($minResp < 20) $hist['10-20 min']++;
|
||||
else $hist['> 20 min']++;
|
||||
} else {
|
||||
$sinRespuesta++;
|
||||
$hist['Sin resp.']++;
|
||||
}
|
||||
|
||||
// Duración de la sesión
|
||||
if (!empty($s['finish_at'])) {
|
||||
$finDt = new DateTime($s['finish_at']);
|
||||
$minDur = round(($finDt->getTimestamp() - $attendDt->getTimestamp()) / 60, 1);
|
||||
if ($minDur >= 0) {
|
||||
$conDuracion++;
|
||||
$sumaMinDuracion += $minDur;
|
||||
$asesores[$opId]['suma_dur'] += $minDur;
|
||||
$asesores[$opId]['con_dur']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$promedioRespuesta = $conRespuesta > 0 ? round($sumaMinRespuesta / $conRespuesta, 1) : null;
|
||||
$promedioDuracion = $conDuracion > 0 ? round($sumaMinDuracion / $conDuracion, 1) : null;
|
||||
$pctRapidas = $totalSesiones > 0 ? round(($hist['< 1 min'] + $hist['1-3 min']) / $totalSesiones * 100) : 0;
|
||||
|
||||
// Formatear asesores
|
||||
$listaAsesores = [];
|
||||
foreach ($asesores as $a) {
|
||||
$listaAsesores[] = [
|
||||
'operador' => $a['operador'],
|
||||
'sesiones' => $a['sesiones'],
|
||||
'prom_respuesta' => $a['con_resp'] > 0 ? round($a['suma_resp'] / $a['con_resp'], 1) : null,
|
||||
'prom_duracion' => $a['con_dur'] > 0 ? round($a['suma_dur'] / $a['con_dur'], 1) : null,
|
||||
];
|
||||
}
|
||||
usort($listaAsesores, fn($a, $b) => ($a['prom_respuesta'] ?? 9999) <=> ($b['prom_respuesta'] ?? 9999));
|
||||
|
||||
// Rellenar días sin actividad en el rango
|
||||
$cur = new DateTime($desde);
|
||||
$fin = new DateTime($hasta);
|
||||
while ($cur <= $fin) {
|
||||
$f = $cur->format('Y-m-d');
|
||||
if (!isset($porDia[$f])) $porDia[$f] = ['fecha' => $f, 'sesiones' => 0, 'suma_resp' => 0, 'con_resp' => 0];
|
||||
$cur->modify('+1 day');
|
||||
}
|
||||
ksort($porDia);
|
||||
|
||||
$serieDia = array_map(fn($d) => [
|
||||
'fecha' => $d['fecha'],
|
||||
'sesiones' => $d['sesiones'],
|
||||
'prom_respuesta' => $d['con_resp'] > 0 ? round($d['suma_resp'] / $d['con_resp'], 1) : null,
|
||||
], array_values($porDia));
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'desde' => $desde,
|
||||
'hasta' => $hasta,
|
||||
'totales' => [
|
||||
'sesiones' => $totalSesiones,
|
||||
'con_respuesta' => $conRespuesta,
|
||||
'sin_respuesta' => $sinRespuesta,
|
||||
'prom_respuesta' => $promedioRespuesta,
|
||||
'prom_duracion' => $promedioDuracion,
|
||||
'pct_rapidas' => $pctRapidas,
|
||||
],
|
||||
'asesores' => $listaAsesores,
|
||||
'histograma' => array_map(fn($k, $v) => ['rango' => $k, 'sesiones' => $v], array_keys($hist), $hist),
|
||||
'serie_dia' => $serieDia,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
Reference in New Issue
Block a user