OmniBoard/app/Http/Controllers/Api/AmiEventController.php

328 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Tenant;
use App\Models\Agent;
use App\Models\Queue;
use App\Models\WaitingList;
use App\Models\DailyMetric;
use Carbon\Carbon;
use App\Events\DashboardUpdate;
class AmiEventController extends Controller
{
public function handle(Request $request)
{
$token = $request->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);
}
$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)
{
$isPaused = ($data['Paused'] ?? '0') == '1';
$asteriskStatus = $data['Status'] ?? null;
$targetStatus = 'offline';
$targetReason = null;
if ($isPaused) {
$targetStatus = 'paused';
$targetReason = !empty($data['PausedReason']) ? $data['PausedReason'] : ($agent->pause_reason ?? 'Pausa');
} else {
$targetReason = null;
switch ($asteriskStatus) {
case '1':
$targetStatus = 'available';
break;
case '2':
case '3':
case '6':
$targetStatus = 'talking';
break;
case '5':
$targetStatus = 'offline';
break;
default:
$targetStatus = 'offline';
break;
}
}
// TRAVA DE IDEMPOTÊNCIA (Igual ao handlePause)
if ($agent->status === $targetStatus && $agent->pause_reason === $targetReason) {
return;
}
$agent->status = $targetStatus;
$agent->pause_reason = $targetReason;
$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)
{
$isPaused = isset($data['Paused']) && $data['Paused'] == '1';
$targetStatus = $isPaused ? 'paused' : 'available'; // Se despausar, assume livre
$targetReason = null;
if ($isPaused) {
$targetReason = $data['Reason'] ?? $data['PausedReason'] ?? 'Pausa';
if (trim($targetReason) === '') $targetReason = 'Pausa';
}
// --- TRAVA DE IDEMPOTÊNCIA ---
// Se o agente está em 10 filas, esse evento dispara 10 vezes.
// Se o status já for igual, IGNORA para não spamar o WebSocket.
if ($agent->status === $targetStatus && $agent->pause_reason === $targetReason) {
return;
}
$agent->status = $targetStatus;
$agent->pause_reason = $targetReason;
$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)
{
$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;
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));
}
}