header('X-Tenant-Token'); $tenant = Tenant::where('api_key', $token)->first(); if (!$tenant) { return response()->json(['error' => 'Unauthorized'], 401); } $data = $request->all(); $eventName = $data['Event'] ?? null; $queueNumber = $data['Queue'] ?? null; // 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); } if ($interface) { $this->saveAgent($interface, $tenant); } if (!$eventName || !$queueNumber) { return response()->json(['status' => 'ignored']); } // Recupera Fila $queue = Queue::where('tenant_id', $tenant->id) ->where('source_id', $queueNumber) ->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': // Foco: Mudar para 'talking' e atualizar métricas $this->handleConnect($queue, $data); break; 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) { WaitingList::create([ 'tenant_id' => $queue->tenant_id, 'queue_id' => $queue->id, 'caller_number' => $data['CallerIDNum'], 'caller_name' => $data['CallerIDName'] ?? 'Desconhecido', 'entered_at' => now(), ]); $this->updateMetric($queue, 'received_count', 1); $this->broadcastUpdate($queue); } // ========================================================================= // 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) { // Lógica de extração de nome $name = $interface; if (strpos($interface, '/') !== false) { $parts = explode('/', $interface); $name = end($parts); } Agent::create([ 'tenant_id' => $tenant->id, 'name' => "Agente $name", 'interface' => $interface, '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( [ 'queue_id' => $queue->id, 'date' => Carbon::today() ], [ 'tenant_id' => $queue->tenant_id, 'received_count' => 0, 'answered_count' => 0, 'abandoned_count' => 0 ] ); } private function updateMetric($queue, $field, $value) { $metric = $this->getTodayMetric($queue); $metric->increment($field, $value); } private function broadcastUpdate($queueOrTenantId) { $tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId; broadcast(new DashboardUpdate($tenantId)); } }