213 lines
7.9 KiB
PHP
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()]);
|
|
}
|