From e907250ac1428e1b6f43fccfd837a89192912ec7 Mon Sep 17 00:00:00 2001 From: lukibeg Date: Sat, 20 Dec 2025 10:04:00 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20Separa=C3=A7=C3=A3o=20de=20agentes=20por?= =?UTF-8?q?=20tenant.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/AmiEventController.php | 331 +++++++++++------- app/Models/Agent.php | 9 +- 2 files changed, 204 insertions(+), 136 deletions(-) diff --git a/app/Http/Controllers/Api/AmiEventController.php b/app/Http/Controllers/Api/AmiEventController.php index f1a9bda..93f26e3 100644 --- a/app/Http/Controllers/Api/AmiEventController.php +++ b/app/Http/Controllers/Api/AmiEventController.php @@ -26,8 +26,11 @@ public function handle(Request $request) $data = $request->all(); $eventName = $data['Event'] ?? 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) { $this->saveQueues($queueNumber, $tenant); } @@ -39,52 +42,187 @@ public function handle(Request $request) return response()->json(['status' => 'ignored']); } - $queue = null; - if ($queueNumber) { - $queue = Queue::where('tenant_id', $tenant->id) + // Recupera Fila + $queue = Queue::where('tenant_id', $tenant->id) ->where('source_id', $queueNumber) ->first(); - } - - // Recupera o Agente (se aplicável) - $agent = null; - if ($interface) { - $agent = Agent::where('tenant_id', $tenant->id) - ->where('interface', $interface) - ->first(); - } if (!$queue) { 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) { case 'QueueCallerJoin': $this->handleJoin($queue, $data); break; + case 'AgentConnect': - $this->handleConnect($queue, $data); + // Foco: Mudar para 'talking' e atualizar métricas + $this->handleConnect($queue, $data); break; - case 'AgentComplete': // Ou QueueCallerLeave com TalkTime - $this->handleComplete($queue, $data); + + case 'AgentComplete': + // Foco: Encerrar contagem de tempo de fala + $this->handleComplete($queue, $data); break; + case 'QueueCallerAbandon': $this->handleAbandon($queue, $data); break; case 'QueueMemberPause': + // Evento explícito de pausa (Alguém clicou no botão de pausa) if ($agent) $this->handlePause($agent, $data, $tenant->id); break; case 'AgentRingNoAnswer': if ($agent) $this->handleRingNoAnswer($agent, $tenant->id); 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']); } + // ========================================================================= + // 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) { @@ -100,94 +238,23 @@ private function handleJoin($queue, $data) $this->broadcastUpdate($queue); } - private function handleConnect($queue, $data) - { - // 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)); - } + // ========================================================================= + // AUXILIARES + // ========================================================================= 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) ->where('interface', $interface) ->exists(); if (!$exists) { - // Tenta limpar o nome. Ex: PJSIP/200 -> 200 + // Lógica de extração de nome $name = $interface; if (strpos($interface, '/') !== false) { $parts = explode('/', $interface); @@ -198,11 +265,42 @@ private function saveAgent($interface, $tenant) 'tenant_id' => $tenant->id, 'name' => "Agente $name", '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) { return DailyMetric::firstOrCreate( @@ -225,46 +323,9 @@ private function updateMetric($queue, $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) { $tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId; broadcast(new DashboardUpdate($tenantId)); } -} +} \ No newline at end of file diff --git a/app/Models/Agent.php b/app/Models/Agent.php index 1866304..df406bb 100644 --- a/app/Models/Agent.php +++ b/app/Models/Agent.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Agent extends Model { - use HasFactory; + use HasFactory, BelongsToTenant; protected $fillable = [ 'tenant_id', @@ -24,4 +25,10 @@ class Agent extends Model protected $casts = [ 'last_status_change' => 'datetime', ]; + + // Opcional, mas recomendado: Relacionamento inverso + public function tenant() + { + return $this->belongsTo(Tenant::class); + } }