fix: Separação de agentes por tenant.

This commit is contained in:
lukibeg 2025-12-20 10:04:00 -03:00
parent 9522db290e
commit e907250ac1
2 changed files with 204 additions and 136 deletions

View File

@ -26,8 +26,11 @@ public function handle(Request $request)
$data = $request->all(); $data = $request->all();
$eventName = $data['Event'] ?? null; $eventName = $data['Event'] ?? null;
$queueNumber = $data['Queue'] ?? null; $queueNumber = $data['Queue'] ?? null;
$interface = $data['Interface'] ?? $data['MemberName'] ?? null; // Tenta achar quem é o agente
// Tenta achar a interface. Se for MemberStatus, vem em 'Interface'. Se for AgentConnect, vem em 'Interface' ou 'MemberName'
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
// Auto-discovery de Filas e Agentes
if ($queueNumber) { if ($queueNumber) {
$this->saveQueues($queueNumber, $tenant); $this->saveQueues($queueNumber, $tenant);
} }
@ -39,52 +42,187 @@ public function handle(Request $request)
return response()->json(['status' => 'ignored']); return response()->json(['status' => 'ignored']);
} }
$queue = null; // Recupera Fila
if ($queueNumber) {
$queue = Queue::where('tenant_id', $tenant->id) $queue = Queue::where('tenant_id', $tenant->id)
->where('source_id', $queueNumber) ->where('source_id', $queueNumber)
->first(); ->first();
}
// Recupera o Agente (se aplicável)
$agent = null;
if ($interface) {
$agent = Agent::where('tenant_id', $tenant->id)
->where('interface', $interface)
->first();
}
if (!$queue) { if (!$queue) {
return response()->json(['error' => 'Fila não encontrada'], 404); return response()->json(['error' => 'Fila não encontrada'], 404);
} }
// Recupera Agente (se aplicável e se não for canal Local)
$agent = $this->findAgent($tenant->id, $data);
switch ($eventName) { switch ($eventName) {
case 'QueueCallerJoin': case 'QueueCallerJoin':
$this->handleJoin($queue, $data); $this->handleJoin($queue, $data);
break; break;
case 'AgentConnect': case 'AgentConnect':
// Foco: Mudar para 'talking' e atualizar métricas
$this->handleConnect($queue, $data); $this->handleConnect($queue, $data);
break; break;
case 'AgentComplete': // Ou QueueCallerLeave com TalkTime
case 'AgentComplete':
// Foco: Encerrar contagem de tempo de fala
$this->handleComplete($queue, $data); $this->handleComplete($queue, $data);
break; break;
case 'QueueCallerAbandon': case 'QueueCallerAbandon':
$this->handleAbandon($queue, $data); $this->handleAbandon($queue, $data);
break; break;
case 'QueueMemberPause': case 'QueueMemberPause':
// Evento explícito de pausa (Alguém clicou no botão de pausa)
if ($agent) $this->handlePause($agent, $data, $tenant->id); if ($agent) $this->handlePause($agent, $data, $tenant->id);
break; break;
case 'AgentRingNoAnswer': case 'AgentRingNoAnswer':
if ($agent) $this->handleRingNoAnswer($agent, $tenant->id); if ($agent) $this->handleRingNoAnswer($agent, $tenant->id);
break; break;
case 'QueueMemberStatus':
// Foco: Atualizar Presença (Offline, Disponível, Pausado, Busy)
if ($agent) $this->handleMemberStatus($agent, $data, $tenant->id);
break;
} }
return response()->json(['status' => 'success']); return response()->json(['status' => 'success']);
} }
// =========================================================================
// HANDLERS
// =========================================================================
private function handleMemberStatus($agent, $data, $tenantId)
{
// 1. A PAUSA TEM PRIORIDADE ABSOLUTA
// Se o Asterisk diz que Paused é '1', o agente está pausado,
// não importa se o telefone está no gancho ou fora do gancho.
if (($data['Paused'] ?? '0') == '1') {
$agent->status = 'paused';
// Só atualizamos o motivo se o evento trouxer um motivo novo.
// O QueueMemberStatus as vezes não traz o motivo, então evitamos apagar o existente.
if (!empty($data['PausedReason'])) {
$agent->pause_reason = $data['PausedReason'];
} elseif (!$agent->pause_reason) {
$agent->pause_reason = 'Pausa'; // Fallback se estiver nulo
}
} else {
// 2. SE NÃO ESTÁ PAUSADO, OLHAMOS O STATUS DO DISPOSITIVO
$agent->pause_reason = null;
$asteriskStatus = $data['Status'] ?? null;
switch ($asteriskStatus) {
case '1': // AST_DEVICE_NOT_INUSE (Livre)
$agent->status = 'available';
break;
case '2': // AST_DEVICE_INUSE (Em uso / Falando)
case '3': // AST_DEVICE_BUSY (Ocupado)
case '6': // AST_DEVICE_RINGING (Tocando)
// Aqui mantemos 'talking' para ficar azul no dashboard
$agent->status = 'talking';
break;
case '5': // AST_DEVICE_UNAVAILABLE (Offline / Cabo desconectado / Softphone fechado)
$agent->status = 'offline';
break;
default:
// Se vier status desconhecido (4, 0, etc), mantemos o atual ou definimos offline
// $agent->status = 'offline';
break;
}
}
$agent->last_status_change = now();
$agent->save();
broadcast(new DashboardUpdate($tenantId));
}
private function handleConnect($queue, $data)
{
// 1. Remove da Lista de Espera
WaitingList::where('queue_id', $queue->id)
->where('caller_number', $data['CallerIDNum'])
->delete();
// 2. Atualiza Métricas da Fila
$metric = $this->getTodayMetric($queue);
$holdTime = intval($data['HoldTime'] ?? 0);
$newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1);
$metric->avg_wait_time = $newAvg;
$metric->answered_count += 1;
$metric->save();
// 3. Atualiza Agente para Talking (Feedback Visual Imediato)
$agent = $this->findAgent($queue->tenant_id, $data);
if ($agent) {
$agent->status = 'talking';
$agent->last_status_change = now();
$agent->increment('total_calls_answered');
$agent->save();
}
$this->broadcastUpdate($queue);
}
private function handleComplete($queue, $data)
{
// Atualiza tempo médio falado
$talkTime = intval($data['TalkTime'] ?? 0);
$metric = $this->getTodayMetric($queue);
if ($metric->answered_count > 0) {
$newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count;
$metric->avg_talk_time = $newAvg;
$metric->save();
}
// NOTA: Removemos a lógica de mudar status para 'available' aqui.
// Deixamos o 'QueueMemberStatus' cuidar disso quando o telefone for colocado no gancho.
// Isso evita conflitos se o agente desligar e imediatamente receber outra chamada.
$this->broadcastUpdate($queue);
}
private function handlePause($agent, $data, $tenantId)
{
// Esse evento ocorre quando a ação de pausa é disparada
$isPaused = isset($data['Paused']) && $data['Paused'] == '1';
$agent->status = $isPaused ? 'paused' : 'available';
$agent->pause_reason = $isPaused ? ($data['Reason'] ?? 'Pausa') : null;
$agent->last_status_change = now();
$agent->save();
broadcast(new DashboardUpdate($tenantId));
}
private function handleAbandon($queue, $data)
{
WaitingList::where('queue_id', $queue->id)
->where('caller_number', $data['CallerIDNum'])
->delete();
$this->updateMetric($queue, 'abandoned_count', 1);
$this->broadcastUpdate($queue);
}
private function handleRingNoAnswer($agent, $tenantId)
{
$agent->increment('total_ring_no_answer');
broadcast(new DashboardUpdate($tenantId));
}
private function handleJoin($queue, $data) private function handleJoin($queue, $data)
{ {
@ -100,94 +238,23 @@ private function handleJoin($queue, $data)
$this->broadcastUpdate($queue); $this->broadcastUpdate($queue);
} }
private function handleConnect($queue, $data) // =========================================================================
{ // AUXILIARES
// 1. Remove da Lista de Espera (Já existente) // =========================================================================
WaitingList::where('queue_id', $queue->id)
->where('caller_number', $data['CallerIDNum'])
->delete();
// 2. Atualiza Métricas da Fila (Já existente)
$metric = $this->getTodayMetric($queue);
$holdTime = intval($data['HoldTime'] ?? 0);
$newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1);
$metric->avg_wait_time = $newAvg;
$metric->answered_count += 1;
$metric->save();
// --- NOVO: Atualiza o Agente ---
$agent = $this->findAgent($queue->tenant_id, $data);
if ($agent) {
$agent->status = 'talking'; // Muda cor para Azul
$agent->last_status_change = now();
$agent->increment('total_calls_answered'); // KPI pessoal do agente
$agent->save();
}
// -------------------------------
$this->broadcastUpdate($queue);
}
private function handleComplete($queue, $data)
{
// 1. Atualiza Métricas da Fila (Já existente)
$talkTime = intval($data['TalkTime'] ?? 0);
$metric = $this->getTodayMetric($queue);
if ($metric->answered_count > 0) {
$newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count;
$metric->avg_talk_time = $newAvg;
$metric->save();
}
// --- NOVO: Libera o Agente ---
$agent = $this->findAgent($queue->tenant_id, $data);
if ($agent) {
// Só volta para available se ele não estiver pausado (fluxo simples)
// O ideal seria checar se ele pausou DURANTE a chamada, mas
// geralmente ao desligar ele volta a ficar livre.
$agent->status = 'available'; // Muda cor para Verde
$agent->last_status_change = now();
$agent->save();
}
// -----------------------------
$this->broadcastUpdate($queue);
}
private function handleAbandon($queue, $data)
{
WaitingList::where('queue_id', $queue->id)
->where('caller_number', $data['CallerIDNum'])
->delete();
$this->updateMetric($queue, 'abandoned_count', 1);
$this->broadcastUpdate($queue);
}
private function handlePause($agent, $data, $tenantId)
{
$isPaused = isset($data['Paused']) && $data['Paused'] == '1';
// Atualiza o Status ENUM e o Motivo
$agent->status = $isPaused ? 'paused' : 'available'; // Define se está pausado ou disponível
$agent->pause_reason = $isPaused ? ($data['Reason'] ?? 'Pausa') : null;
$agent->last_status_change = now(); // Atualiza o relógio
$agent->save();
broadcast(new DashboardUpdate($tenantId));
}
private function saveAgent($interface, $tenant) private function saveAgent($interface, $tenant)
{ {
// Ignora canais locais para não sujar o banco
if (str_starts_with($interface, 'Local/')) {
return;
}
$exists = Agent::where('tenant_id', $tenant->id) $exists = Agent::where('tenant_id', $tenant->id)
->where('interface', $interface) ->where('interface', $interface)
->exists(); ->exists();
if (!$exists) { if (!$exists) {
// Tenta limpar o nome. Ex: PJSIP/200 -> 200 // Lógica de extração de nome
$name = $interface; $name = $interface;
if (strpos($interface, '/') !== false) { if (strpos($interface, '/') !== false) {
$parts = explode('/', $interface); $parts = explode('/', $interface);
@ -198,11 +265,42 @@ private function saveAgent($interface, $tenant)
'tenant_id' => $tenant->id, 'tenant_id' => $tenant->id,
'name' => "Agente $name", 'name' => "Agente $name",
'interface' => $interface, 'interface' => $interface,
'status' => 'available' // Assume disponível ao ser descoberto 'status' => 'available'
]); ]);
} }
} }
private function saveQueues($queueSourceId, $tenant)
{
$exists = Queue::where('tenant_id', $tenant->id)
->where('source_id', $queueSourceId)
->exists();
if (!$exists) {
Queue::create([
'tenant_id' => $tenant->id,
'type' => 'voice',
'source_id' => $queueSourceId,
'name' => "Fila $queueSourceId",
'sector' => null
]);
}
}
private function findAgent($tenantId, $data)
{
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
// Se for canal Local/ ou nulo, retorna null para não processar
if (!$interface || str_starts_with($interface, 'Local/')) {
return null;
}
return Agent::where('tenant_id', $tenantId)
->where('interface', $interface)
->first();
}
private function getTodayMetric($queue) private function getTodayMetric($queue)
{ {
return DailyMetric::firstOrCreate( return DailyMetric::firstOrCreate(
@ -225,43 +323,6 @@ private function updateMetric($queue, $field, $value)
$metric->increment($field, $value); $metric->increment($field, $value);
} }
private function handleRingNoAnswer($agent, $tenantId)
{
// Incrementa contador de chamadas perdidas pelo agente
$agent->increment('total_ring_no_answer');
broadcast(new DashboardUpdate($tenantId));
}
private function saveQueues($queue, $tenant)
{
$existingQueue = Queue::where('tenant_id', $tenant->id)
->where('source_id', $queue)
->exists();
if (!$existingQueue) {
Queue::create([
'tenant_id' => $tenant->id,
'type' => 'voice',
'source_id' => $queue,
'name' => "Fila $queue", // Nome padrão provisório
'sector' => null // Começa sem setor
]);
}
}
// --- COLOQUE AQUI NO FINAL, JUNTO COM OS OUTROS PRIVATES ---
private function findAgent($tenantId, $data)
{
// Tenta achar o identificador do agente no evento
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
if (!$interface) return null;
return Agent::where('tenant_id', $tenantId)
->where('interface', $interface)
->first();
}
private function broadcastUpdate($queueOrTenantId) private function broadcastUpdate($queueOrTenantId)
{ {
$tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId; $tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId;

View File

@ -2,12 +2,13 @@
namespace App\Models; namespace App\Models;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Agent extends Model class Agent extends Model
{ {
use HasFactory; use HasFactory, BelongsToTenant;
protected $fillable = [ protected $fillable = [
'tenant_id', 'tenant_id',
@ -24,4 +25,10 @@ class Agent extends Model
protected $casts = [ protected $casts = [
'last_status_change' => 'datetime', 'last_status_change' => 'datetime',
]; ];
// Opcional, mas recomendado: Relacionamento inverso
public function tenant()
{
return $this->belongsTo(Tenant::class);
}
} }