Files
whatsapp/api/kpi_respuesta_asesor.php
2026-05-11 12:21:56 -05:00

213 lines
7.9 KiB
PHP

<?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()]);
}